Skip to content

Commit

Permalink
[breaking] New() returns error; supports Options.
Browse files Browse the repository at this point in the history
- New now has a variadic opts argument that accepts a slice of Option.
- Options for changing the underlying http.Client and http.Header map added.
- Updated flarectl to handle the updated New() signature.
- A new Error() interface for returning different errors, and wrapping the
  cause, to provide easier inspection of errors without having to type-assert
  for all possible causes.
  • Loading branch information
elithrar committed May 6, 2016
1 parent 44261e5 commit 586e5a5
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 25 deletions.
71 changes: 48 additions & 23 deletions cloudflare.go
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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.
Expand Down Expand Up @@ -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{}
Expand All @@ -92,26 +114,27 @@ 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"`
Count int `json:"count"`
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"`
Expand All @@ -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"`
Expand All @@ -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"`
Expand All @@ -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"`
Expand Down
2 changes: 1 addition & 1 deletion cloudflare_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func setup() {
server = httptest.NewServer(mux)

// CloudFlare client configured to use test server
client = New("[email protected]", "deadbeef")
client, _ = New("[email protected]", "deadbeef")
client.BaseURL = server.URL
}

Expand Down
8 changes: 7 additions & 1 deletion cmd/flarectl/flarectl.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"errors"
"fmt"
"log"
"os"
"reflect"
"strings"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
35 changes: 35 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
@@ -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
}
39 changes: 39 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 586e5a5

Please sign in to comment.