Skip to content

Commit

Permalink
refactored to avoid reaching API batch request limit; go fmtd
Browse files Browse the repository at this point in the history
  • Loading branch information
awilliams committed Jun 17, 2014
1 parent 844832d commit 81da508
Show file tree
Hide file tree
Showing 4 changed files with 373 additions and 313 deletions.
194 changes: 86 additions & 108 deletions api/api.go
Original file line number Diff line number Diff line change
@@ -1,141 +1,119 @@
package api

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
)

const (
API_URL = "https://api.linode.com/"
ApiUrl = "https://api.linode.com/"
MaxBatchRequests = 25
)

type queryParams map[string]string

type apiAction struct {
method string
params queryParams
method string
params queryParams
}

func (self *apiAction) Set(key, value string) {
self.params[key] = value
self.params[key] = value
}

func (self apiAction) values() queryParams {
self.params["api_action"] = self.method
return self.params
self.params["api_action"] = self.method
return self.params
}

type apiRequest struct {
apiKey string
baseUrl *url.URL
actions []*apiAction
apiKey string
baseUrl *url.URL
actions []*apiAction
}

func NewApiRequest(apiKey string) (*apiRequest, error) {
apiUrl, err := url.Parse(API_URL)
if err != nil {
return nil, err
}
var actions []*apiAction
return &apiRequest{apiKey: apiKey, baseUrl: apiUrl, actions: actions}, nil
apiUrl, err := url.Parse(ApiUrl)
if err != nil {
return nil, err
}
var actions []*apiAction
return &apiRequest{apiKey: apiKey, baseUrl: apiUrl, actions: actions}, nil
}

func (self *apiRequest) AddAction(method string) *apiAction {
action := &apiAction{method: method, params: make(queryParams)}
self.actions = append(self.actions, action)
return action
action := &apiAction{method: method, params: make(queryParams)}
self.actions = append(self.actions, action)
return action
}

func (self apiRequest) URL() string {
params := make(url.Values)
params.Set("api_key", self.apiKey)

if len(self.actions) == 1 {
for key, value := range self.actions[0].values() {
params.Set(key, value)
}
} else if len(self.actions) > 1 {
params.Set("api_action", "batch")
var requestArray []queryParams
for _, action := range self.actions {
requestArray = append(requestArray, action.values())
}
b, err := json.Marshal(requestArray)
if err != nil {
log.Fatal(err)
}
params.Set("api_requestArray", string(b))
}
self.baseUrl.RawQuery = params.Encode()
return self.baseUrl.String()
func (self apiRequest) URL() []string {
actionBatches := make([][]*apiAction, (len(self.actions)/MaxBatchRequests)+1)
for j, action := range self.actions {
i := j / MaxBatchRequests
actionBatches[i] = append(actionBatches[i], action)
}

var urls []string
for _, actions := range actionBatches {
params := make(url.Values)
params.Set("api_key", self.apiKey)
params.Set("api_action", "batch")
var requestArray []queryParams
for _, action := range actions {
requestArray = append(requestArray, action.values())
}
b, _ := json.Marshal(requestArray)
params.Set("api_requestArray", string(b))
u := self.baseUrl
u.RawQuery = params.Encode()
urls = append(urls, u.String())
}
return urls
}

func (self apiRequest) GetJson(data interface{}) error {
resp, err := http.Get(self.URL())
if err != nil {
return err
}
defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}

// the linode API does not use HTTP status codes to indicate errors,
// rather it embeds in the JSON document the errors. When there is an error
// the foramt of the `DATA` element changes as well, which would cause the json decode to fail.
//
// Here we first parse the json to see if it contains errors, then re-parse with the provided
// json structure
if linodeErr := checkForLinodeError(bytes.NewReader(body)); linodeErr != nil {
return linodeErr
}

decoder := json.NewDecoder(bytes.NewReader(body))
err = decoder.Decode(data)
if err != nil {
return err
}

return nil
type apiResponse struct {
Action string `json:"ACTION"`
Errors []struct {
Code int `json:"ERRORCODE"`
Message string `json:"ERRORMESSAGE"`
} `json:"ERRORARRAY,omitempty"`
Data json.RawMessage `json:"DATA,omitempty"`
}

type errorJson struct {
Errors []struct {
Code int `json:"ERRORCODE"`
Message string `json:"ERRORMESSAGE"`
} `json:"ERRORARRAY,omitempty"`
}

func checkForLinodeError(body *bytes.Reader) error {
data := new(errorJson)
decoder := json.NewDecoder(body)
err := decoder.Decode(&data)
if err != nil {
// this is not actually an error, since there is not always an error present in the JSON
return nil
}
if len(data.Errors) > 0 {
var buf bytes.Buffer
buf.WriteString("Api Error!\n")
for _, e := range data.Errors {
buf.WriteString(fmt.Sprintf("[Code: %d] %s\n", e.Code, e.Message))
}
return fmt.Errorf(buf.String())
}
return nil
}

func (self *apiRequest) GoString() string {
s, err := url.QueryUnescape(self.URL())
if err != nil {
return ""
}
return s
func (self apiRequest) GetJson() ([]json.RawMessage, []error) {
var datas []json.RawMessage
var errs []error

for _, url := range self.URL() {
resp, err := http.Get(url)
if err != nil {
return nil, []error{err}
}
defer resp.Body.Close()

decoder := json.NewDecoder(resp.Body)

var apiResponses []apiResponse
err = decoder.Decode(&apiResponses)
if err != nil {
return nil, []error{err}
}

for _, apiResp := range apiResponses {
// Check for 'ERROR' attribute for any values, which would indicate an error
if len(apiResp.Errors) > 0 {
for _, e := range apiResp.Errors {
errs = append(errs, errors.New(fmt.Sprintf("[Code: %d] %s\n", e.Code, e.Message)))
}
continue
}
datas = append(datas, apiResp.Data)
}
}

return datas, errs
}
Loading

0 comments on commit 81da508

Please sign in to comment.