diff --git a/cloudflare.go b/cloudflare.go index 21e1f77e961..8048440163a 100644 --- a/cloudflare.go +++ b/cloudflare.go @@ -1,11 +1,4 @@ -/* -Package cloudflare implements the CloudFlare v4 API. - -New API requests created like: - - api := cloudflare.New(apikey, apiemail) - -*/ +// Package cloudflare implements the CloudFlare v4 API. package cloudflare import ( @@ -24,15 +17,39 @@ const apiURL = "https://api.cloudflare.com/client/v4" const errMakeRequestError = "Error from makeRequest" const errUnmarshalError = "Error unmarshalling JSON" +// API holds the configuration for the current API client. A client should not +// be modified concurrently. type API struct { - APIKey string - APIEmail string - BaseURL string + APIKey string + APIEmail string + BaseURL string + headers http.Header + httpClient *http.Client } -// Initializes the API configuration. -func New(key, email string) *API { - return &API{key, email, apiURL} +// New creates a new CloudFlare v4 API client. +func New(key, email string, opts ...Option) (*API, error) { + if key == "" || email == "" { + return nil, UserError{errEmptyCredentials} + } + + api := &API{ + APIKey: key, + APIEmail: email, + } + + err := api.parseOptions(opts...) + if err != nil { + return nil, UserError{err} + } + + // Fall back to http.DefaultClient if the package user does not provide + // their own. + if api.httpClient == nil { + api.httpClient = http.DefaultClient + } + + return api, nil } // Initializes a new zone. @@ -72,8 +89,13 @@ func (api *API) makeRequest(method, uri string, params interface{}) ([]byte, err if err != nil { return nil, errors.Wrap(err, "HTTP request creation failed") } - req.Header.Add("X-Auth-Key", api.APIKey) - req.Header.Add("X-Auth-Email", api.APIEmail) + + // Apply any user-defined headers first. + req.Header = api.headers + + req.Header.Set("X-Auth-Key", api.APIKey) + req.Header.Set("X-Auth-Email", api.APIEmail) + // Could be application/json or multipart/form-data // req.Header.Add("Content-Type", "application/json") client := &http.Client{} @@ -92,18 +114,19 @@ func (api *API) makeRequest(method, uri string, params interface{}) ([]byte, err return nil, errors.New(resp.Status) } } + return resBody, nil } -// The Response struct is a template. There will also be a result struct. -// There will be a unique response type for each response, which will include -// this type. +// Response is a template. There will also be a result struct. There will be a +// unique response type for each response, which will include this type. type Response struct { Success bool `json:"success"` Errors []string `json:"errors"` Messages []string `json:"messages"` } +// ResultInfo contains metadata about the Response. type ResultInfo struct { Page int `json:"page"` PerPage int `json:"per_page"` @@ -111,7 +134,7 @@ type ResultInfo struct { Total int `json:"total_count"` } -// A User describes a user account. +// User describes a user account. type User struct { ID string `json:"id"` Email string `json:"email"` @@ -129,18 +152,20 @@ type User struct { Organizations []Organization `json:"organizations"` } +// UserResponse wraps a response containing User accounts. type UserResponse struct { Response Result User `json:"result"` } +// Owner describes the resource owner. type Owner struct { ID string `json:"id"` Email string `json:"email"` OwnerType string `json:"owner_type"` } -// A Zone describes a CloudFlare zone. +// Zone describes a CloudFlare zone. type Zone struct { ID string `json:"id"` Name string `json:"name"` @@ -167,7 +192,7 @@ type Zone struct { Meta ZoneMeta `json:"meta"` } -// Contains metadata about a zone. +// ZoneMeta metadata about a zone. type ZoneMeta struct { // custom_certificate_quota is broken - sometimes it's a string, sometimes a number! // CustCertQuota int `json:"custom_certificate_quota"` @@ -176,7 +201,7 @@ type ZoneMeta struct { PhishingDetected bool `json:"phishing_detected"` } -// Contains the plan information for a zone. +// ZonePlan contains the plan information for a zone. type ZonePlan struct { ID string `json:"id"` Name string `json:"name"` diff --git a/cloudflare_test.go b/cloudflare_test.go index eafb88c53fb..823192c2714 100644 --- a/cloudflare_test.go +++ b/cloudflare_test.go @@ -26,7 +26,7 @@ func setup() { server = httptest.NewServer(mux) // CloudFlare client configured to use test server - client = New("cloudflare@example.org", "deadbeef") + client, _ = New("cloudflare@example.org", "deadbeef") client.BaseURL = server.URL } diff --git a/cmd/flarectl/flarectl.go b/cmd/flarectl/flarectl.go index 44c2b5603a1..7664fd3e3aa 100644 --- a/cmd/flarectl/flarectl.go +++ b/cmd/flarectl/flarectl.go @@ -3,6 +3,7 @@ package main import ( "errors" "fmt" + "log" "os" "reflect" "strings" @@ -423,6 +424,7 @@ func pageRules(c *cli.Context) { fmt.Println(err) return } + fmt.Printf("%3s %-32s %-8s %s\n", "Pri", "ID", "Status", "URL") for _, r := range rules { var settings []string @@ -453,7 +455,11 @@ func railgun(*cli.Context) { } func main() { - api = cloudflare.New(os.Getenv("CF_API_KEY"), os.Getenv("CF_API_EMAIL")) + var err error + api, err = cloudflare.New(os.Getenv("CF_API_KEY"), os.Getenv("CF_API_EMAIL")) + if err != nil { + log.Fatal(err) + } app := cli.NewApp() app.Name = "flarectl" diff --git a/errors.go b/errors.go new file mode 100644 index 00000000000..c2264af296b --- /dev/null +++ b/errors.go @@ -0,0 +1,35 @@ +package cloudflare + +import "errors" + +var errEmptyCredentials = errors.New("invalid credentials: key & email must not be empty") + +// Error represents an error returned from this library. +type Error interface { + // Raised when user credentials or configuration is invalid. + User() bool + // Raised when a network error occurs. + Network() bool + // Contains the original (wrapped) error. + Cause() error +} + +// UserError represents a user-generated error. +type UserError struct { + error +} + +// User is a user-caused error. +func (e UserError) User() bool { + return true +} + +// Network error. +func (e UserError) Network() bool { + return false +} + +// Cause wraps the underlying error. +func (e UserError) Cause() error { + return e.error +} diff --git a/options.go b/options.go new file mode 100644 index 00000000000..33ad18c1d67 --- /dev/null +++ b/options.go @@ -0,0 +1,39 @@ +package cloudflare + +import "net/http" + +// Option is a functional option for configuring the API client. +type Option func(*API) error + +// HTTPClient accepts a custom *http.Client for making API calls. +func HTTPClient(client *http.Client) Option { + return func(api *API) error { + api.httpClient = client + return nil + } +} + +// Headers allows you to set custom HTTP headers when making API calls (e.g. for +// satisfying HTTP proxies, or for debugging). +func Headers(headers http.Header) Option { + return func(api *API) error { + api.headers = headers + return nil + } +} + +// parseOptions parses the supplied options functions and returns a configured +// *API instance. +func (api *API) parseOptions(opts ...Option) error { + // Range over each options function and apply it to our API type to + // configure it. Options functions are applied in order, with any + // conflicting options overriding earlier calls. + for _, option := range opts { + err := option(api) + if err != nil { + return err + } + } + + return nil +}