github.com/1and1/oneandone-cloudserver-sdk-go@v1.4.1/restclient.go (about)

     1  package oneandone
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/http"
    11  	p_url "net/url"
    12  	"time"
    13  )
    14  
    15  type restClient struct {
    16  	token     string
    17  	transport http.RoundTripper
    18  }
    19  
    20  func newRestClient(token string) *restClient {
    21  	restClient := new(restClient)
    22  	restClient.token = token
    23  	return restClient
    24  }
    25  
    26  func (c *restClient) SetTransport(transport http.RoundTripper) {
    27  	c.transport = transport
    28  }
    29  
    30  func (c *restClient) Get(url string, result interface{}, expectedStatus int) error {
    31  	return c.doRequest(url, "GET", nil, result, expectedStatus)
    32  }
    33  
    34  func (c *restClient) Delete(url string, requestBody interface{}, result interface{}, expectedStatus int) error {
    35  	return c.doRequest(url, "DELETE", requestBody, result, expectedStatus)
    36  }
    37  
    38  func (c *restClient) Post(url string, requestBody interface{}, result interface{}, expectedStatus int) error {
    39  	return c.doRequest(url, "POST", requestBody, result, expectedStatus)
    40  }
    41  
    42  func (c *restClient) Put(url string, requestBody interface{}, result interface{}, expectedStatus int) error {
    43  	return c.doRequest(url, "PUT", requestBody, result, expectedStatus)
    44  }
    45  
    46  func (c *restClient) doRequest(url string, method string, requestBody interface{}, result interface{}, expectedStatus int) error {
    47  	maxRetries := 5
    48  	retriesCount := 0
    49  
    50  	for {
    51  
    52  		var bodyData io.Reader
    53  		if requestBody != nil {
    54  			data, err := json.Marshal(requestBody)
    55  			if err != nil {
    56  				return err
    57  			}
    58  			bodyData = bytes.NewBuffer(data)
    59  		}
    60  		request, err := http.NewRequest(method, url, bodyData)
    61  		if err != nil {
    62  			return err
    63  		}
    64  		request.Header.Add("X-Token", c.token)
    65  		request.Header.Add("Content-Type", "application/json")
    66  
    67  		client := http.Client{}
    68  		if c.transport != nil {
    69  			client.Transport = c.transport
    70  		}
    71  
    72  		response, err := client.Do(request)
    73  
    74  		if err != nil {
    75  			_, ok := err.(*p_url.Error)
    76  			if ok {
    77  				//EOF error, sending a retry
    78  				if retriesCount < maxRetries {
    79  					retriesCount++
    80  					time.Sleep(3 * time.Second)
    81  					continue
    82  				}
    83  			}
    84  			return err
    85  		}
    86  
    87  		if response != nil {
    88  			defer response.Body.Close()
    89  			body, err := ioutil.ReadAll(response.Body)
    90  			if err != nil {
    91  				return err
    92  			}
    93  			if response.StatusCode == http.StatusTooManyRequests {
    94  				retryAfter := response.Header.Get("X-Rate-Limit-Reset")
    95  				if retryAfter == "" {
    96  					//let the SDK sleep for 60 seconds while the API send the correct header
    97  					retryAfter = "60"
    98  				}
    99  
   100  				sleep, err := time.ParseDuration(retryAfter + "s")
   101  				if err != nil {
   102  					return err
   103  				}
   104  
   105  				time.Sleep(sleep)
   106  
   107  			} else if response.StatusCode != expectedStatus {
   108  				erResp := &errorResponse{}
   109  				err = json.Unmarshal(body, erResp)
   110  				if err != nil {
   111  					return err
   112  				}
   113  				return ApiError{response.StatusCode, fmt.Sprintf("Type: %s; Message: %s", erResp.Type, erResp.Message)}
   114  			} else {
   115  				return c.unmarshal(body, result)
   116  			}
   117  		}
   118  	}
   119  }
   120  
   121  func (c *restClient) unmarshal(data []byte, result interface{}) error {
   122  	err := json.Unmarshal(data, result)
   123  	if err != nil {
   124  		// handle the case when the result is an empty array instead of an object
   125  		switch err.(type) {
   126  		case *json.UnmarshalTypeError:
   127  			var ra []interface{}
   128  			e := json.Unmarshal(data, &ra)
   129  			if e != nil {
   130  				return e
   131  			} else if len(ra) > 0 {
   132  				return err
   133  			}
   134  			return nil
   135  		default:
   136  			return err
   137  		}
   138  	}
   139  
   140  	return nil
   141  }
   142  
   143  func createUrl(api *API, sections ...interface{}) string {
   144  	url := api.Endpoint
   145  	for _, section := range sections {
   146  		url += "/" + fmt.Sprint(section)
   147  	}
   148  	return url
   149  }
   150  
   151  func makeParameterMap(args ...interface{}) (map[string]interface{}, error) {
   152  	qps := make(map[string]interface{}, len(args))
   153  	var is_true bool
   154  	var page, per_page int
   155  	var sort, query, fields string
   156  
   157  	for i, p := range args {
   158  		switch i {
   159  		case 0:
   160  			page, is_true = p.(int)
   161  			if !is_true {
   162  				return nil, errors.New("1st parameter must be a page number (integer).")
   163  			} else if page > 0 {
   164  				qps["page"] = page
   165  			}
   166  		case 1:
   167  			per_page, is_true = p.(int)
   168  			if !is_true {
   169  				return nil, errors.New("2nd parameter must be a per_page number (integer).")
   170  			} else if per_page > 0 {
   171  				qps["per_page"] = per_page
   172  			}
   173  		case 2:
   174  			sort, is_true = p.(string)
   175  			if !is_true {
   176  				return nil, errors.New("3rd parameter must be a sorting property string (e.g. 'name' or '-name').")
   177  			} else if sort != "" {
   178  				qps["sort"] = sort
   179  			}
   180  		case 3:
   181  			query, is_true = p.(string)
   182  			if !is_true {
   183  				return nil, errors.New("4th parameter must be a query string to look for the response.")
   184  			} else if query != "" {
   185  				qps["q"] = query
   186  			}
   187  		case 4:
   188  			fields, is_true = p.(string)
   189  			if !is_true {
   190  				return nil, errors.New("5th parameter must be fields properties string (e.g. 'id,name').")
   191  			} else if fields != "" {
   192  				qps["fields"] = fields
   193  			}
   194  		default:
   195  			return nil, errors.New("Wrong number of parameters.")
   196  		}
   197  	}
   198  	return qps, nil
   199  }
   200  
   201  func processQueryParams(url string, args ...interface{}) (string, error) {
   202  	if len(args) > 0 {
   203  		params, err := makeParameterMap(args...)
   204  		if err != nil {
   205  			return "", err
   206  		}
   207  		url = appendQueryParams(url, params)
   208  	}
   209  	return url, nil
   210  }
   211  
   212  func processQueryParamsExt(url string, period string, sd *time.Time, ed *time.Time, args ...interface{}) (string, error) {
   213  	var qm map[string]interface{}
   214  	var err error
   215  	if len(args) > 0 {
   216  		qm, err = makeParameterMap(args...)
   217  		if err != nil {
   218  			return "", err
   219  		}
   220  	} else {
   221  		qm = make(map[string]interface{}, 3)
   222  	}
   223  	qm["period"] = period
   224  	if sd != nil && ed != nil {
   225  		if sd.After(*ed) {
   226  			return "", errors.New("Start date cannot be after end date.")
   227  		}
   228  		qm["start_date"] = sd.Format(time.RFC3339)
   229  		qm["end_date"] = ed.Format(time.RFC3339)
   230  	}
   231  	url = appendQueryParams(url, qm)
   232  	return url, nil
   233  }
   234  
   235  func appendQueryParams(url string, params map[string]interface{}) string {
   236  	queryUrl, _ := p_url.Parse(url)
   237  	parameters := p_url.Values{}
   238  	for key, value := range params {
   239  		parameters.Add(key, fmt.Sprintf("%v", value))
   240  	}
   241  	queryUrl.RawQuery = parameters.Encode()
   242  	return queryUrl.String()
   243  }