packngo.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. package packngo
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "io/ioutil"
  8. "net/http"
  9. "net/url"
  10. "strconv"
  11. "strings"
  12. "time"
  13. )
  14. const (
  15. libraryVersion = "0.1.0"
  16. baseURL = "https://api.packet.net/"
  17. userAgent = "packngo/" + libraryVersion
  18. mediaType = "application/json"
  19. headerRateLimit = "X-RateLimit-Limit"
  20. headerRateRemaining = "X-RateLimit-Remaining"
  21. headerRateReset = "X-RateLimit-Reset"
  22. )
  23. // ListOptions specifies optional global API parameters
  24. type ListOptions struct {
  25. // for paginated result sets, page of results to retrieve
  26. Page int `url:"page,omitempty"`
  27. // for paginated result sets, the number of results to return per page
  28. PerPage int `url:"per_page,omitempty"`
  29. // specify which resources you want to return as collections instead of references
  30. Includes string
  31. }
  32. // Response is the http response from api calls
  33. type Response struct {
  34. *http.Response
  35. Rate
  36. }
  37. func (r *Response) populateRate() {
  38. // parse the rate limit headers and populate Response.Rate
  39. if limit := r.Header.Get(headerRateLimit); limit != "" {
  40. r.Rate.RequestLimit, _ = strconv.Atoi(limit)
  41. }
  42. if remaining := r.Header.Get(headerRateRemaining); remaining != "" {
  43. r.Rate.RequestsRemaining, _ = strconv.Atoi(remaining)
  44. }
  45. if reset := r.Header.Get(headerRateReset); reset != "" {
  46. if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 {
  47. r.Rate.Reset = Timestamp{time.Unix(v, 0)}
  48. }
  49. }
  50. }
  51. // ErrorResponse is the http response used on errrors
  52. type ErrorResponse struct {
  53. Response *http.Response
  54. Errors []string `json:"errors"`
  55. }
  56. func (r *ErrorResponse) Error() string {
  57. return fmt.Sprintf("%v %v: %d %v",
  58. r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, strings.Join(r.Errors, ", "))
  59. }
  60. // Client is the base API Client
  61. type Client struct {
  62. client *http.Client
  63. BaseURL *url.URL
  64. UserAgent string
  65. ConsumerToken string
  66. APIKey string
  67. RateLimit Rate
  68. // Packet Api Objects
  69. Plans PlanService
  70. Users UserService
  71. Emails EmailService
  72. SSHKeys SSHKeyService
  73. Devices DeviceService
  74. Projects ProjectService
  75. Facilities FacilityService
  76. OperatingSystems OSService
  77. }
  78. // NewRequest inits a new http request with the proper headers
  79. func (c *Client) NewRequest(method, path string, body interface{}) (*http.Request, error) {
  80. // relative path to append to the endpoint url, no leading slash please
  81. rel, err := url.Parse(path)
  82. if err != nil {
  83. return nil, err
  84. }
  85. u := c.BaseURL.ResolveReference(rel)
  86. // json encode the request body, if any
  87. buf := new(bytes.Buffer)
  88. if body != nil {
  89. err := json.NewEncoder(buf).Encode(body)
  90. if err != nil {
  91. return nil, err
  92. }
  93. }
  94. req, err := http.NewRequest(method, u.String(), buf)
  95. if err != nil {
  96. return nil, err
  97. }
  98. req.Close = true
  99. req.Header.Add("X-Auth-Token", c.APIKey)
  100. req.Header.Add("X-Consumer-Token", c.ConsumerToken)
  101. req.Header.Add("Content-Type", mediaType)
  102. req.Header.Add("Accept", mediaType)
  103. req.Header.Add("User-Agent", userAgent)
  104. return req, nil
  105. }
  106. // Do executes the http request
  107. func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
  108. resp, err := c.client.Do(req)
  109. if err != nil {
  110. return nil, err
  111. }
  112. defer resp.Body.Close()
  113. response := Response{Response: resp}
  114. response.populateRate()
  115. c.RateLimit = response.Rate
  116. err = checkResponse(resp)
  117. // if the response is an error, return the ErrorResponse
  118. if err != nil {
  119. return &response, err
  120. }
  121. if v != nil {
  122. // if v implements the io.Writer interface, return the raw response
  123. if w, ok := v.(io.Writer); ok {
  124. io.Copy(w, resp.Body)
  125. } else {
  126. err = json.NewDecoder(resp.Body).Decode(v)
  127. if err != nil {
  128. return &response, err
  129. }
  130. }
  131. }
  132. return &response, err
  133. }
  134. // NewClient initializes and returns a Client, use this to get an API Client to operate on
  135. // N.B.: Packet's API certificate requires Go 1.5+ to successfully parse. If you are using
  136. // an older version of Go, pass in a custom http.Client with a custom TLS configuration
  137. // that sets "InsecureSkipVerify" to "true"
  138. func NewClient(consumerToken string, apiKey string, httpClient *http.Client) *Client {
  139. if httpClient == nil {
  140. // Don't fall back on http.DefaultClient as it's not nice to adjust state
  141. // implicitly. If the client wants to use http.DefaultClient, they can
  142. // pass it in explicitly.
  143. httpClient = &http.Client{}
  144. }
  145. BaseURL, _ := url.Parse(baseURL)
  146. c := &Client{client: httpClient, BaseURL: BaseURL, UserAgent: userAgent, ConsumerToken: consumerToken, APIKey: apiKey}
  147. c.Plans = &PlanServiceOp{client: c}
  148. c.Users = &UserServiceOp{client: c}
  149. c.Emails = &EmailServiceOp{client: c}
  150. c.SSHKeys = &SSHKeyServiceOp{client: c}
  151. c.Devices = &DeviceServiceOp{client: c}
  152. c.Projects = &ProjectServiceOp{client: c}
  153. c.Facilities = &FacilityServiceOp{client: c}
  154. c.OperatingSystems = &OSServiceOp{client: c}
  155. return c
  156. }
  157. func checkResponse(r *http.Response) error {
  158. // return if http status code is within 200 range
  159. if c := r.StatusCode; c >= 200 && c <= 299 {
  160. // response is good, return
  161. return nil
  162. }
  163. errorResponse := &ErrorResponse{Response: r}
  164. data, err := ioutil.ReadAll(r.Body)
  165. // if the response has a body, populate the message in errorResponse
  166. if err == nil && len(data) > 0 {
  167. json.Unmarshal(data, errorResponse)
  168. }
  169. return errorResponse
  170. }