diff --git a/.gitignore b/.gitignore index f1c181e..556c3b2 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out + +# Bin +target/ +target/* \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4b0f3bc --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: go +go: + - 1.10.x +before_install: + - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh + - dep ensure + +script: make travis \ No newline at end of file diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 0000000..82e82bb --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,17 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + digest = "1:a63cff6b5d8b95638bfe300385d93b2a6d9d687734b863da8e09dc834510a690" + name = "github.com/google/go-querystring" + packages = ["query"] + pruneopts = "UT" + revision = "44c6ddd0a2342c386950e880b658017258da92fc" + version = "v1.0.0" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + input-imports = ["github.com/google/go-querystring/query"] + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 0000000..f9f576d --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,38 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + + +[[constraint]] + name = "github.com/davecgh/go-spew" + version = "1.1.1" + +[prune] + go-tests = true + unused-packages = true + +[[constraint]] + name = "github.com/google/go-querystring" + version = "1.0.0" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..df7cf50 --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +# GoWinds Makefile +#///////////////////////# +#/// DEFS ///# +#///////////////////////# +# Don't ask, for to understand it is to look +# into the void and know the void is not only +#looking back but also reading your emails. +SHELL=/bin/bash -e -o pipefail + +# ENV Vars defaults +GOOS ?= darwin +GOARCH ?= amd64 +VERSION ?= v0.1 + +#///////////////////////# +#/// OUTPUT ///# +#///////////////////////# + +RED=\033[0;31m +GREEN=\033[0;32m +YELLOW=\033[01;33m +BLUE=\033[0;34m +LBLUE=\033[01;34m +ORANGE=\033[0;33m +PURPLE=\033[0;35m +LCYAN=\033[1;36m +NC=\033[0m + +#///////////////////////# +#/// TARGETS ///# +#///////////////////////# + +.PHONY: help + +help: ## Show this help. + @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' + +.PHONY: test cover build buildall clean + +travis: clean test build + +test: ## Run test with coverage + go test -v -race -cover -coverprofile=cov.out + +cover: ## Open coverage + go tool cover --html=cov.out + +build: ## Build for testing + go build -o target/gowinds + +clean: ## Clean build + @go clean + @rm -rf target + @rm -rf cov.out \ No newline at end of file diff --git a/README.md b/README.md index dacfbf2..e76b294 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # GoWinds +[![Build Status](https://travis-ci.org/openwurl/gowinds.svg?branch=master)](https://travis-ci.org/openwurl/gowinds) A basic Go client to HighWinds CDN API \ No newline at end of file diff --git a/analytics/analytics.go b/analytics/analytics.go new file mode 100644 index 0000000..9f1f8cb --- /dev/null +++ b/analytics/analytics.go @@ -0,0 +1,6 @@ +package analytics + +// Service interface defines the available analytics methods +type Service interface { + //GetAllXFer(reqOpt *RequestOptions, anaOpt *AnalyticsOptions) (*StatusObject, error) // /analytics/transfer +} diff --git a/client.go b/client.go new file mode 100644 index 0000000..14b6c51 --- /dev/null +++ b/client.go @@ -0,0 +1,163 @@ +package gowinds + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + + "github.com/google/go-querystring/query" +) + +const ( + baseURL = "https://striketracker.highwinds.com" + basePath = "api/v1/accounts" + mediaType = "application/json" + applicationID = "gowinds" +) + +// RequestOptions specifies global API parameters for every call +type RequestOptions struct { + // AccountHash is required and variable + AccountHash string `url:"account_hash,omitempty"` +} + +// createURL concatenates the account hash at request time +func (r *RequestOptions) createURL() string { + url := fmt.Sprintf("%s/%s", basePath, r.AccountHash) + return url +} + +// Response is the API call response +type Response struct { + *http.Response +} + +// Logger interface is the default logger +type logger interface { + Printf(string, ...interface{}) +} + +// Client is our core universal API client +type Client struct { + client *http.Client + debug bool + AuthorizationHeaderToken string + BaseURL *url.URL + logger + // Services +} + +// SetLogger sets the logger +func (c *Client) SetLogger(l logger) error { + c.logger = l + return nil +} + +// SetDebug enables/disables debug logging after creating a client +func (c *Client) SetDebug(toggle bool) { + c.debug = toggle + return +} + +// SetBaseURL Sets optional BaseURL just in case - mostly for tests +func (c *Client) SetBaseURL(u string) error { + url, err := url.Parse(u) + if err != nil { + return err + } + c.BaseURL = url + return nil +} + +// NewClient returns a copy of the client +func NewClient(authorizationHeaderToken string) (*Client, error) { + + // Fetch auth data + if authorizationHeaderToken == "" || len(authorizationHeaderToken) == 0 { + return nil, fmt.Errorf("authorizationHeaderToken required") + } + + // URL object parsed + u, err := url.Parse(baseURL) + if err != nil { + return nil, err + } + + c := &Client{client: &http.Client{}, AuthorizationHeaderToken: authorizationHeaderToken, BaseURL: u, debug: false} + // Mount services + // c.Analytics + // c.Hosts + return c, nil +} + +// NewRequest packgs a new http request +func (c *Client) NewRequest(method, path string, body interface{}) (*http.Request, error) { + // validate and have obj + rel, err := url.Parse(path) + if err != nil { + return nil, err + } + + // build url and queries + u := c.BaseURL.ResolveReference(rel) + v, _ := query.Values(body) + u.RawQuery = v.Encode() + + // create raw request, body is not used on striketracker + req, err := http.NewRequest(method, u.String(), nil) + if err != nil { + return nil, err + } + + if c.debug { + c.logger.Printf("Request: ", req) + } + + req.Close = true + + // pack headers + req.Header.Add("Authorization", c.AuthorizationHeaderToken) + req.Header.Add("X-Application-Id", applicationID) + req.Header.Add("Content-Type", mediaType) + + return req, nil +} + +// Do fires the request +func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + response := Response{Response: resp} + + if v != nil { + // Just in case we use an io.Writer to decode elsewhere + if w, ok := v.(io.Writer); ok { + io.Copy(w, resp.Body) + } else { + // Decode in the struct injected in v + err = json.NewDecoder(resp.Body).Decode(v) + if err != nil { + return &response, err + } + } + } + + return &response, err +} + +// DoRequest creates and fires a request +func (c *Client) DoRequest(method, path string, body, v interface{}) (*Response, error) { + req, err := c.NewRequest(method, path, body) + if err != nil { + return nil, err + } + + return c.Do(req, v) +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..8bdce8f --- /dev/null +++ b/client_test.go @@ -0,0 +1,109 @@ +package gowinds + +import ( + "bytes" + "fmt" + "log" + "net/http" + "net/http/httptest" + "reflect" + "testing" +) + +var coreAPIStub = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var resp string + switch r.RequestURI { + case "/api/v1/accounts/default/hosts/brokenhash": + resp = ` + { + "Error": "Does not exist" + } + ` + case "/api/v1/accounts/default/hosts/expectedhash": + resp = ` + { + "HashCode": "expectedhash", + "Name": "Host01", + "Error": nil + } + ` + } + w.Write([]byte(resp)) +})) + +func TestNewClient(t *testing.T) { + c, err := NewClient("") + if err == nil { + t.Fatal("expected new client instantiation to fail, instead got client: ", c) + } + + testToken := "TestToken" + c, err = NewClient(testToken) + if err != nil { + t.Fatal("failed to instantiate new client: ", err) + } + + if c.AuthorizationHeaderToken != testToken { + t.Fatalf("expected c.AuthorizationHeaderToken to be %s, got %s", testToken, c.AuthorizationHeaderToken) + } + + var testLogger logger + if reflect.TypeOf(c.logger) != reflect.TypeOf(testLogger) { + t.Fatalf("expected c.logger to be %v, got %v", reflect.TypeOf(testLogger), c.logger) + } + + var buf bytes.Buffer + logger := log.New(&buf, "logger: ", log.Lshortfile) + c.SetLogger(logger) + + if !reflect.DeepEqual(logger, c.logger) { + t.Fatalf("expected c.logger to be %v, got %v", logger, c.logger) + } + + c.SetBaseURL(coreAPIStub.URL) + + if c.BaseURL.String() != coreAPIStub.URL { + t.Fatalf("Expected c.BaseURL to be %v, got %v", coreAPIStub.URL, c.BaseURL) + } + +} + +//func TestNewRequest(t *testing.T) { +// +//} + +//func TestDo(t *testing.T) { +// +//} + +func TestDoRequest(t *testing.T) { + testToken := "TestToken" + c, err := NewClient(testToken) + if err != nil { + t.Fatal("failed to instantiate new client: ", err) + } + + // purposefully bad URL + err = c.SetBaseURL("postgres://user:abc{DEf1=ghi@example.com:5432/db?sslmode=require") + if err == nil { + t.Fatalf("expected c.SetBaseURL to error, got: %v - %v", c.BaseURL, err) + } + + err = c.SetBaseURL(coreAPIStub.URL) + if err != nil { + t.Fatalf("expected c.SetBaseURL to not error, got: %v", err) + } + + reqOpt := RequestOptions{ + AccountHash: "default", + } + var hostResponse interface{} + method := "GET" + path := fmt.Sprintf("%s/default/hosts/brokenhash", reqOpt.createURL()) + resp, err := c.DoRequest(method, path, nil, hostResponse) + if err != nil { + t.Fatalf("expected err to be nil, got %v", err) + } + t.Log(resp) + // This will be expanded once we have things to put it into and properly analyze +}