Update to current API spec
* Allow requesting pages in lists by number * Add tooling to allow examination of page links if they exist * Add domain management CRUD
This commit is contained in:
parent
6a5163c77e
commit
96accbf5e9
76
README.md
76
README.md
|
@ -1,11 +1,16 @@
|
|||
# GODO
|
||||
# Godo
|
||||
|
||||
Godo is a Go client library for accessing the DigitalOcean V2 API.
|
||||
|
||||
You can view the client API docs here: [http://godoc.org/github.com/digitalocean/godo](http://godoc.org/github.com/digitalocean/godo)
|
||||
|
||||
You can view Digital Ocean API docs here: [https://developers.digitalocean.com/v2/](https://developers.digitalocean.com/v2/)
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
import "github.com/digitaloceancloud/godo"
|
||||
import "github.com/digitalocean/godo"
|
||||
```
|
||||
|
||||
Create a new DigitalOcean client, then use the exposed services to
|
||||
|
@ -32,20 +37,6 @@ client := godo.NewClient(t.Client())
|
|||
|
||||
## Examples
|
||||
|
||||
[Digital Ocean API Documentation](https://developers.digitalocean.com/v2/)
|
||||
|
||||
|
||||
To list all Droplets your account has access to:
|
||||
|
||||
```go
|
||||
droplets, _, err := client.Droplet.List()
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n\n", err)
|
||||
return err
|
||||
} else {
|
||||
fmt.Printf("%v\n\n", godo.Stringify(droplets))
|
||||
}
|
||||
```
|
||||
|
||||
To create a new Droplet:
|
||||
|
||||
|
@ -53,16 +44,57 @@ To create a new Droplet:
|
|||
dropletName := "super-cool-droplet"
|
||||
|
||||
createRequest := &godo.DropletCreateRequest{
|
||||
Name: godo.String(dropletName),
|
||||
Region: godo.String("nyc2"),
|
||||
Size: godo.String("512mb"),
|
||||
Image: godo.Int(3240036), // ubuntu 14.04 64bit
|
||||
Name: godo.String(dropletName),
|
||||
Region: godo.String("nyc2"),
|
||||
Size: godo.String("512mb"),
|
||||
Image: godo.Int(3240036), // ubuntu 14.04 64bit
|
||||
}
|
||||
|
||||
newDroplet, _, err := client.Droplet.Create(createRequest)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Something bad happened: %s\n\n", err)
|
||||
return err
|
||||
fmt.Printf("Something bad happened: %s\n\n", err)
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
If a list of items is paginated by the API, you must request pages individually. For example, to fetch all Droplets:
|
||||
|
||||
```go
|
||||
func DropletList(client *godo.Client) ([]godo.Droplet, error) {
|
||||
// create a list to hold our droplets
|
||||
list := []godo.Droplet{}
|
||||
|
||||
// create options. initially, these will be blank
|
||||
opt := &godo.ListOptions{}
|
||||
for {
|
||||
droplets, resp, err := client.Droplets.List(opt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// append the current page's droplets to our list
|
||||
for _, d := range droplets {
|
||||
list = append(list, d)
|
||||
}
|
||||
|
||||
// if we are at the last page, break out the for loop
|
||||
if resp.Links.IsLastPage() {
|
||||
break
|
||||
}
|
||||
|
||||
page, err := resp.Links.CurrentPage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set the page we want for the next request
|
||||
opt.Page = page + 1
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
```
|
||||
|
|
12
action.go
12
action.go
|
@ -15,7 +15,7 @@ const (
|
|||
// ActionsService handles communction with action related methods of the
|
||||
// DigitalOcean API: https://developers.digitalocean.com/#actions
|
||||
type ActionsService interface {
|
||||
List() ([]Action, *Response, error)
|
||||
List(*ListOptions) ([]Action, *Response, error)
|
||||
Get(int) (*Action, *Response, error)
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,7 @@ type ActionsServiceOp struct {
|
|||
|
||||
type actionsRoot struct {
|
||||
Actions []Action `json:"actions"`
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
type actionRoot struct {
|
||||
|
@ -45,8 +46,12 @@ type Action struct {
|
|||
}
|
||||
|
||||
// List all actions
|
||||
func (s *ActionsServiceOp) List() ([]Action, *Response, error) {
|
||||
func (s *ActionsServiceOp) List(opt *ListOptions) ([]Action, *Response, error) {
|
||||
path := actionsBasePath
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest("GET", path, nil)
|
||||
if err != nil {
|
||||
|
@ -58,6 +63,9 @@ func (s *ActionsServiceOp) List() ([]Action, *Response, error) {
|
|||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Actions, resp, err
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ func TestAction_List(t *testing.T) {
|
|||
testMethod(t, r, "GET")
|
||||
})
|
||||
|
||||
actions, _, err := client.Actions.List()
|
||||
actions, _, err := client.Actions.List(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
@ -33,6 +33,54 @@ func TestAction_List(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAction_ListActionMultiplePages(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/v2/actions", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, `{"actions": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/droplets/?page=2"}}}`)
|
||||
testMethod(t, r, "GET")
|
||||
})
|
||||
|
||||
_, resp, err := client.Actions.List(nil)
|
||||
if err != nil {
|
||||
t.Fatal(nil)
|
||||
}
|
||||
|
||||
checkCurrentPage(t, resp, 1)
|
||||
}
|
||||
|
||||
func TestAction_RetrievePageByNumber(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
jBlob := `
|
||||
{
|
||||
"actions": [{"id":1},{"id":2}],
|
||||
"links":{
|
||||
"pages":{
|
||||
"next":"http://example.com/v2/actions/?page=3",
|
||||
"prev":"http://example.com/v2/actions/?page=1",
|
||||
"last":"http://example.com/v2/actions/?page=3",
|
||||
"first":"http://example.com/v2/actions/?page=1"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
mux.HandleFunc("/v2/actions", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
fmt.Fprint(w, jBlob)
|
||||
})
|
||||
|
||||
opt := &ListOptions{Page: 2}
|
||||
_, resp, err := client.Actions.List(opt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkCurrentPage(t, resp, 2)
|
||||
}
|
||||
|
||||
func TestAction_Get(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
|
136
domains.go
136
domains.go
|
@ -8,7 +8,12 @@ const domainsBasePath = "v2/domains"
|
|||
// See: https://developers.digitalocean.com/#domains and
|
||||
// https://developers.digitalocean.com/#domain-records
|
||||
type DomainsService interface {
|
||||
Records(string, *DomainRecordsOptions) ([]DomainRecord, *Response, error)
|
||||
List(*ListOptions) ([]Domain, *Response, error)
|
||||
Get(string) (*DomainRoot, *Response, error)
|
||||
Create(*DomainCreateRequest) (*DomainRoot, *Response, error)
|
||||
Delete(string) (*Response, error)
|
||||
|
||||
Records(string, *ListOptions) ([]DomainRecord, *Response, error)
|
||||
Record(string, int) (*DomainRecord, *Response, error)
|
||||
DeleteRecord(string, int) (*Response, error)
|
||||
EditRecord(string, int, *DomainRecordEditRequest) (*DomainRecord, *Response, error)
|
||||
|
@ -21,6 +26,29 @@ type DomainsServiceOp struct {
|
|||
client *Client
|
||||
}
|
||||
|
||||
// Domain represents a Digital Ocean domain
|
||||
type Domain struct {
|
||||
Name string `json:"name"`
|
||||
TTL int `json:"ttl"`
|
||||
ZoneFile string `json:"zone_file"`
|
||||
}
|
||||
|
||||
// DomainRoot represents a response from the Digital Ocean API
|
||||
type DomainRoot struct {
|
||||
Domain *Domain `json:"domain"`
|
||||
}
|
||||
|
||||
type domainsRoot struct {
|
||||
Domains []Domain `json:"domains"`
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
// DomainCreateRequest respresents a request to create a domain.
|
||||
type DomainCreateRequest struct {
|
||||
Name string `json:"name"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
}
|
||||
|
||||
// DomainRecordRoot is the root of an individual Domain Record response
|
||||
type DomainRecordRoot struct {
|
||||
DomainRecord *DomainRecord `json:"domain_record"`
|
||||
|
@ -29,6 +57,7 @@ type DomainRecordRoot struct {
|
|||
// DomainRecordsRoot is the root of a group of Domain Record responses
|
||||
type DomainRecordsRoot struct {
|
||||
DomainRecords []DomainRecord `json:"domain_records"`
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
// DomainRecord represents a DigitalOcean DomainRecord
|
||||
|
@ -42,16 +71,6 @@ type DomainRecord struct {
|
|||
Weight int `json:"weight,omitempty"`
|
||||
}
|
||||
|
||||
// DomainRecordsOptions are options for DomainRecords
|
||||
type DomainRecordsOptions struct {
|
||||
ListOptions
|
||||
}
|
||||
|
||||
// Converts a DomainRecord to a string.
|
||||
func (d DomainRecord) String() string {
|
||||
return Stringify(d)
|
||||
}
|
||||
|
||||
// DomainRecordEditRequest represents a request to update a domain record.
|
||||
type DomainRecordEditRequest struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
|
@ -62,13 +81,97 @@ type DomainRecordEditRequest struct {
|
|||
Weight int `json:"weight,omitempty"`
|
||||
}
|
||||
|
||||
func (d Domain) String() string {
|
||||
return Stringify(d)
|
||||
}
|
||||
|
||||
// List all domains
|
||||
func (s DomainsServiceOp) List(opt *ListOptions) ([]Domain, *Response, error) {
|
||||
path := domainsBasePath
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest("GET", path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(domainsRoot)
|
||||
resp, err := s.client.Do(req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Domains, resp, err
|
||||
}
|
||||
|
||||
// Get individual domain
|
||||
func (s *DomainsServiceOp) Get(name string) (*DomainRoot, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", domainsBasePath, name)
|
||||
|
||||
req, err := s.client.NewRequest("GET", path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(DomainRoot)
|
||||
resp, err := s.client.Do(req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, err
|
||||
}
|
||||
|
||||
// Create a new domain
|
||||
func (s *DomainsServiceOp) Create(createRequest *DomainCreateRequest) (*DomainRoot, *Response, error) {
|
||||
path := domainsBasePath
|
||||
|
||||
req, err := s.client.NewRequest("POST", path, createRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(DomainRoot)
|
||||
resp, err := s.client.Do(req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, err
|
||||
}
|
||||
|
||||
// Delete droplet
|
||||
func (s *DomainsServiceOp) Delete(name string) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", domainsBasePath, name)
|
||||
|
||||
req, err := s.client.NewRequest("DELETE", path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Converts a DomainRecord to a string.
|
||||
func (d DomainRecord) String() string {
|
||||
return Stringify(d)
|
||||
}
|
||||
|
||||
// Converts a DomainRecordEditRequest to a string.
|
||||
func (d DomainRecordEditRequest) String() string {
|
||||
return Stringify(d)
|
||||
}
|
||||
|
||||
// Records returns a slice of DomainRecords for a domain
|
||||
func (s *DomainsServiceOp) Records(domain string, opt *DomainRecordsOptions) ([]DomainRecord, *Response, error) {
|
||||
func (s *DomainsServiceOp) Records(domain string, opt *ListOptions) ([]DomainRecord, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s/records", domainsBasePath, domain)
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
|
@ -80,13 +183,16 @@ func (s *DomainsServiceOp) Records(domain string, opt *DomainRecordsOptions) ([]
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
records := new(DomainRecordsRoot)
|
||||
resp, err := s.client.Do(req, records)
|
||||
root := new(DomainRecordsRoot)
|
||||
resp, err := s.client.Do(req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return records.DomainRecords, resp, err
|
||||
return root.DomainRecords, resp, err
|
||||
}
|
||||
|
||||
// Record returns the record id from a domain
|
||||
|
|
145
domains_test.go
145
domains_test.go
|
@ -14,6 +14,149 @@ func TestAction_DomainsServiceOpImplementsDomainsService(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDomains_ListDomains(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/v2/domains", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
fmt.Fprint(w, `{"domains": [{"name":"foo.com"},{"name":"bar.com"}]}`)
|
||||
})
|
||||
|
||||
domains, _, err := client.Domains.List(nil)
|
||||
if err != nil {
|
||||
t.Errorf("Domains.List returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []Domain{{Name: "foo.com"}, {Name: "bar.com"}}
|
||||
if !reflect.DeepEqual(domains, expected) {
|
||||
t.Errorf("Domains.List returned %+v, expected %+v", domains, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomains_ListDomainsMultiplePages(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/v2/domains", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
fmt.Fprint(w, `{"domains": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/domains/?page=2"}}}`)
|
||||
})
|
||||
|
||||
_, resp, err := client.Domains.List(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkCurrentPage(t, resp, 1)
|
||||
}
|
||||
|
||||
func TestDomains_RetrievePageByNumber(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
jBlob := `
|
||||
{
|
||||
"domains": [{"id":1},{"id":2}],
|
||||
"links":{
|
||||
"pages":{
|
||||
"next":"http://example.com/v2/domains/?page=3",
|
||||
"prev":"http://example.com/v2/domains/?page=1",
|
||||
"last":"http://example.com/v2/domains/?page=3",
|
||||
"first":"http://example.com/v2/domains/?page=1"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
mux.HandleFunc("/v2/domains", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
fmt.Fprint(w, jBlob)
|
||||
})
|
||||
|
||||
opt := &ListOptions{Page: 2}
|
||||
_, resp, err := client.Domains.List(opt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkCurrentPage(t, resp, 2)
|
||||
}
|
||||
|
||||
func TestDomains_GetDomain(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/v2/domains/example.com", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
fmt.Fprint(w, `{"domain":{"name":"example.com"}}`)
|
||||
})
|
||||
|
||||
domains, _, err := client.Domains.Get("example.com")
|
||||
if err != nil {
|
||||
t.Errorf("domain.Get returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := &DomainRoot{Domain: &Domain{Name: "example.com"}}
|
||||
if !reflect.DeepEqual(domains, expected) {
|
||||
t.Errorf("domains.Get returned %+v, expected %+v", domains, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomains_Create(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
createRequest := &DomainCreateRequest{
|
||||
Name: "example.com",
|
||||
IPAddress: "127.0.0.1",
|
||||
}
|
||||
|
||||
mux.HandleFunc("/v2/domains", func(w http.ResponseWriter, r *http.Request) {
|
||||
v := new(DomainCreateRequest)
|
||||
err := json.NewDecoder(r.Body).Decode(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testMethod(t, r, "POST")
|
||||
if !reflect.DeepEqual(v, createRequest) {
|
||||
t.Errorf("Request body = %+v, expected %+v", v, createRequest)
|
||||
}
|
||||
|
||||
dr := DomainRoot{&Domain{Name: v.Name}}
|
||||
b, err := json.Marshal(dr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Fprint(w, string(b))
|
||||
})
|
||||
|
||||
domain, _, err := client.Domains.Create(createRequest)
|
||||
if err != nil {
|
||||
t.Errorf("Domains.Create returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := &DomainRoot{Domain: &Domain{Name: "example.com"}}
|
||||
if !reflect.DeepEqual(domain, expected) {
|
||||
t.Errorf("Domains.Create returned %+v, expected %+v", domain, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomains_Destroy(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/v2/domains/example.com", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "DELETE")
|
||||
})
|
||||
|
||||
_, err := client.Domains.Delete("example.com")
|
||||
if err != nil {
|
||||
t.Errorf("Domains.Delete returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomains_AllRecordsForDomainName(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
@ -47,7 +190,7 @@ func TestDomains_AllRecordsForDomainName_PerPage(t *testing.T) {
|
|||
fmt.Fprint(w, `{"domain_records":[{"id":1},{"id":2}]}`)
|
||||
})
|
||||
|
||||
dro := &DomainRecordsOptions{ListOptions{PerPage: 2}}
|
||||
dro := &ListOptions{PerPage: 2}
|
||||
records, _, err := client.Domains.Records("example.com", dro)
|
||||
if err != nil {
|
||||
t.Errorf("Domains.List returned error: %v", err)
|
||||
|
|
41
droplets.go
41
droplets.go
|
@ -8,7 +8,7 @@ const dropletBasePath = "v2/droplets"
|
|||
// endpoints of the Digital Ocean API
|
||||
// See: https://developers.digitalocean.com/#droplets
|
||||
type DropletsService interface {
|
||||
List() ([]Droplet, *Response, error)
|
||||
List(*ListOptions) ([]Droplet, *Response, error)
|
||||
Get(int) (*DropletRoot, *Response, error)
|
||||
Create(*DropletCreateRequest) (*DropletRoot, *Response, error)
|
||||
Delete(int) (*Response, error)
|
||||
|
@ -52,6 +52,7 @@ type DropletRoot struct {
|
|||
|
||||
type dropletsRoot struct {
|
||||
Droplets []Droplet `json:"droplets"`
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
// DropletCreateRequest represents a request to create a droplet.
|
||||
|
@ -85,45 +86,29 @@ func (n Network) String() string {
|
|||
return Stringify(n)
|
||||
}
|
||||
|
||||
// Links are extra links for a droplet
|
||||
type Links struct {
|
||||
Actions []Link `json:"actions,omitempty"`
|
||||
}
|
||||
|
||||
// Action extracts Link
|
||||
func (l *Links) Action(action string) *Link {
|
||||
for _, a := range l.Actions {
|
||||
if a.Rel == action {
|
||||
return &a
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Link represents a link
|
||||
type Link struct {
|
||||
ID int `json:"id,omitempty"`
|
||||
Rel string `json:"rel,omitempty"`
|
||||
HREF string `json:"href,omitempty"`
|
||||
}
|
||||
|
||||
// List all droplets
|
||||
func (s *DropletsServiceOp) List() ([]Droplet, *Response, error) {
|
||||
func (s *DropletsServiceOp) List(opt *ListOptions) ([]Droplet, *Response, error) {
|
||||
path := dropletBasePath
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest("GET", path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
droplets := new(dropletsRoot)
|
||||
resp, err := s.client.Do(req, droplets)
|
||||
root := new(dropletsRoot)
|
||||
resp, err := s.client.Do(req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return droplets.Droplets, resp, err
|
||||
return root.Droplets, resp, err
|
||||
}
|
||||
|
||||
// Get individual droplet
|
||||
|
|
|
@ -23,7 +23,7 @@ func TestDroplets_ListDroplets(t *testing.T) {
|
|||
fmt.Fprint(w, `{"droplets": [{"id":1},{"id":2}]}`)
|
||||
})
|
||||
|
||||
droplets, _, err := client.Droplet.List()
|
||||
droplets, _, err := client.Droplets.List(nil)
|
||||
if err != nil {
|
||||
t.Errorf("Droplets.List returned error: %v", err)
|
||||
}
|
||||
|
@ -34,6 +34,70 @@ func TestDroplets_ListDroplets(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDroplets_ListDropletsMultiplePages(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
|
||||
dr := dropletsRoot{
|
||||
Droplets: []Droplet{
|
||||
Droplet{ID: 1},
|
||||
Droplet{ID: 2},
|
||||
},
|
||||
Links: &Links{
|
||||
Pages: &Pages{Next: "http://example.com/v2/droplets/?page=2"},
|
||||
},
|
||||
}
|
||||
|
||||
b, err := json.Marshal(dr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Fprint(w, string(b))
|
||||
})
|
||||
|
||||
_, resp, err := client.Droplets.List(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkCurrentPage(t, resp, 1)
|
||||
}
|
||||
|
||||
func TestDroplets_RetrievePageByNumber(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
jBlob := `
|
||||
{
|
||||
"droplets": [{"id":1},{"id":2}],
|
||||
"links":{
|
||||
"pages":{
|
||||
"next":"http://example.com/v2/droplets/?page=3",
|
||||
"prev":"http://example.com/v2/droplets/?page=1",
|
||||
"last":"http://example.com/v2/droplets/?page=3",
|
||||
"first":"http://example.com/v2/droplets/?page=1"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
fmt.Fprint(w, jBlob)
|
||||
})
|
||||
|
||||
opt := &ListOptions{Page: 2}
|
||||
_, resp, err := client.Droplets.List(opt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkCurrentPage(t, resp, 2)
|
||||
}
|
||||
|
||||
func TestDroplets_GetDroplet(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
@ -43,7 +107,7 @@ func TestDroplets_GetDroplet(t *testing.T) {
|
|||
fmt.Fprint(w, `{"droplet":{"id":12345}}`)
|
||||
})
|
||||
|
||||
droplets, _, err := client.Droplet.Get(12345)
|
||||
droplets, _, err := client.Droplets.Get(12345)
|
||||
if err != nil {
|
||||
t.Errorf("Droplet.Get returned error: %v", err)
|
||||
}
|
||||
|
@ -77,7 +141,7 @@ func TestDroplets_Create(t *testing.T) {
|
|||
fmt.Fprintf(w, `{"droplet":{"id":1}}`)
|
||||
})
|
||||
|
||||
droplet, _, err := client.Droplet.Create(createRequest)
|
||||
droplet, _, err := client.Droplets.Create(createRequest)
|
||||
if err != nil {
|
||||
t.Errorf("Droplets.Create returned error: %v", err)
|
||||
}
|
||||
|
@ -96,34 +160,12 @@ func TestDroplets_Destroy(t *testing.T) {
|
|||
testMethod(t, r, "DELETE")
|
||||
})
|
||||
|
||||
_, err := client.Droplet.Delete(12345)
|
||||
_, err := client.Droplets.Delete(12345)
|
||||
if err != nil {
|
||||
t.Errorf("Droplet.Delete returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinks_Actions(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
aLink := Link{ID: 1, Rel: "a", HREF: "http://example.com/a"}
|
||||
|
||||
links := Links{
|
||||
Actions: []Link{
|
||||
aLink,
|
||||
Link{ID: 2, Rel: "b", HREF: "http://example.com/b"},
|
||||
Link{ID: 2, Rel: "c", HREF: "http://example.com/c"},
|
||||
},
|
||||
}
|
||||
|
||||
link := links.Action("a")
|
||||
|
||||
if *link != aLink {
|
||||
t.Errorf("expected %+v, got %+v", aLink, link)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNetwork_String(t *testing.T) {
|
||||
network := &Network{
|
||||
IPAddress: "192.168.1.2",
|
||||
|
|
59
godo.go
59
godo.go
|
@ -45,7 +45,7 @@ type Client struct {
|
|||
// Services used for communicating with the API
|
||||
Actions ActionsService
|
||||
Domains DomainsService
|
||||
Droplet DropletsService
|
||||
Droplets DropletsService
|
||||
DropletActions DropletActionsService
|
||||
Images ImagesService
|
||||
ImageActions ImageActionsService
|
||||
|
@ -68,15 +68,9 @@ type ListOptions struct {
|
|||
type Response struct {
|
||||
*http.Response
|
||||
|
||||
// These fields provide the page values for paginating through a set of
|
||||
// results. Any or all of these may be set to the zero value for
|
||||
// responses that are not part of a paginated set, or for which there
|
||||
// are no additional pages.
|
||||
|
||||
NextPage string
|
||||
PrevPage string
|
||||
FirstPage string
|
||||
LastPage string
|
||||
// Links that were returned with the response. These are parsed from
|
||||
// request body and not the header.
|
||||
Links *Links
|
||||
|
||||
// Monitoring URI
|
||||
Monitor string
|
||||
|
@ -137,7 +131,7 @@ func NewClient(httpClient *http.Client) *Client {
|
|||
c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent}
|
||||
c.Actions = &ActionsServiceOp{client: c}
|
||||
c.Domains = &DomainsServiceOp{client: c}
|
||||
c.Droplet = &DropletsServiceOp{client: c}
|
||||
c.Droplets = &DropletsServiceOp{client: c}
|
||||
c.DropletActions = &DropletActionsServiceOp{client: c}
|
||||
c.Images = &ImagesServiceOp{client: c}
|
||||
c.ImageActions = &ImageActionsServiceOp{client: c}
|
||||
|
@ -181,54 +175,11 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ
|
|||
// newResponse creates a new Response for the provided http.Response
|
||||
func newResponse(r *http.Response) *Response {
|
||||
response := Response{Response: r}
|
||||
response.populatePageValues()
|
||||
response.populateRate()
|
||||
response.populateMonitor()
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
// populatePageValues parses the HTTP Link response headers and populates the
|
||||
// various pagination link values in the Response.
|
||||
func (r *Response) populatePageValues() {
|
||||
links, err := r.links()
|
||||
|
||||
if err == nil {
|
||||
var l headerLink.Link
|
||||
var ok bool
|
||||
|
||||
l, ok = links["next"]
|
||||
if ok {
|
||||
r.NextPage = l.URI
|
||||
}
|
||||
l, ok = links["prev"]
|
||||
if ok {
|
||||
r.PrevPage = l.URI
|
||||
}
|
||||
|
||||
l, ok = links["first"]
|
||||
if ok {
|
||||
r.FirstPage = l.URI
|
||||
}
|
||||
|
||||
l, ok = links["last"]
|
||||
if ok {
|
||||
r.LastPage = l.URI
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Response) populateMonitor() {
|
||||
links, err := r.links()
|
||||
|
||||
if err == nil {
|
||||
link, ok := links["monitor"]
|
||||
if ok {
|
||||
r.Monitor = link.URI
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Response) links() (map[string]headerLink.Link, error) {
|
||||
if linkText, ok := r.Response.Header["Link"]; ok {
|
||||
links, err := headerLink.Parse(linkText[0])
|
||||
|
|
80
godo_test.go
80
godo_test.go
|
@ -300,74 +300,6 @@ func TestDo_rateLimit_errorResponse(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResponse_populatePageValues(t *testing.T) {
|
||||
r := http.Response{
|
||||
Header: http.Header{
|
||||
"Link": {`<https://api.digitalocean.com/?page=1>; rel="first",` +
|
||||
` <https://api.digitalocean.com/?page=2>; rel="prev",` +
|
||||
` <https://api.digitalocean.com/?page=4>; rel="next",` +
|
||||
` <https://api.digitalocean.com/?page=5>; rel="last"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
response := newResponse(&r)
|
||||
|
||||
links := map[string]string{
|
||||
"first": "https://api.digitalocean.com/?page=1",
|
||||
"prev": "https://api.digitalocean.com/?page=2",
|
||||
"next": "https://api.digitalocean.com/?page=4",
|
||||
"last": "https://api.digitalocean.com/?page=5",
|
||||
}
|
||||
|
||||
if expected, got := links["first"], response.FirstPage; expected != got {
|
||||
t.Errorf("response.FirstPage: %v, expected %v", got, expected)
|
||||
}
|
||||
if expected, got := links["prev"], response.PrevPage; expected != got {
|
||||
t.Errorf("response.PrevPage: %v, expected %v", got, expected)
|
||||
}
|
||||
if expected, got := links["next"], response.NextPage; expected != got {
|
||||
t.Errorf("response.NextPage: %v, expected %v", got, expected)
|
||||
}
|
||||
if expected, got := links["last"], response.LastPage; expected != got {
|
||||
t.Errorf("response.LastPage: %v, expected %v", got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponse_populatePageValues_invalid(t *testing.T) {
|
||||
r := http.Response{
|
||||
Header: http.Header{
|
||||
"Link": {`<https://api.digitalocean.com/?page=1>,` +
|
||||
`<https://api.digitalocean.com/?page=abc>; rel="first",` +
|
||||
`https://api.digitalocean.com/?page=2; rel="prev",` +
|
||||
`<https://api.digitalocean.com/>; rel="next",` +
|
||||
`<https://api.digitalocean.com/?page=>; rel="last"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
response := newResponse(&r)
|
||||
if expected, got := "", response.FirstPage; expected != got {
|
||||
t.Errorf("response.FirstPage: %v, expected %v", expected, got)
|
||||
}
|
||||
if expected, got := "", response.PrevPage; expected != got {
|
||||
t.Errorf("response.PrevPage: %v, expected %v", expected, got)
|
||||
}
|
||||
if expected, got := "", response.NextPage; expected != got {
|
||||
t.Errorf("response.NextPage: %v, expected %v", expected, got)
|
||||
}
|
||||
if expected, got := "", response.LastPage; expected != got {
|
||||
t.Errorf("response.LastPage: %v, expected %v", expected, got)
|
||||
}
|
||||
|
||||
// more invalid URLs
|
||||
r = http.Response{
|
||||
Header: http.Header{
|
||||
"Link": {`<https://api.digitalocean.com/%?page=2>; rel="first"`},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func Implements(interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) bool {
|
||||
interfaceType := reflect.TypeOf(interfaceObject).Elem()
|
||||
if !reflect.TypeOf(object).Implements(interfaceType) {
|
||||
|
@ -376,3 +308,15 @@ func Implements(interfaceObject interface{}, object interface{}, msgAndArgs ...i
|
|||
|
||||
return true
|
||||
}
|
||||
|
||||
func checkCurrentPage(t *testing.T, resp *Response, expectedPage int) {
|
||||
links := resp.Links
|
||||
p, err := links.CurrentPage()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if p != expectedPage {
|
||||
t.Fatalf("expected current page to be '%d', was '%d'", expectedPage, p)
|
||||
}
|
||||
}
|
||||
|
|
18
images.go
18
images.go
|
@ -4,7 +4,7 @@ package godo
|
|||
// endpoints of the Digital Ocean API
|
||||
// See: https://developers.digitalocean.com/#images
|
||||
type ImagesService interface {
|
||||
List() ([]Image, *Response, error)
|
||||
List(*ListOptions) ([]Image, *Response, error)
|
||||
}
|
||||
|
||||
// ImagesServiceOp handles communication with the image related methods of the
|
||||
|
@ -29,6 +29,7 @@ type imageRoot struct {
|
|||
|
||||
type imagesRoot struct {
|
||||
Images []Image
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
func (i Image) String() string {
|
||||
|
@ -36,19 +37,26 @@ func (i Image) String() string {
|
|||
}
|
||||
|
||||
// List all sizes
|
||||
func (s *ImagesServiceOp) List() ([]Image, *Response, error) {
|
||||
func (s *ImagesServiceOp) List(opt *ListOptions) ([]Image, *Response, error) {
|
||||
path := "v2/images"
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest("GET", path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
images := new(imagesRoot)
|
||||
resp, err := s.client.Do(req, images)
|
||||
root := new(imagesRoot)
|
||||
resp, err := s.client.Do(req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return images.Images, resp, err
|
||||
return root.Images, resp, err
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ func TestImages_List(t *testing.T) {
|
|||
fmt.Fprint(w, `{"images":[{"id":1},{"id":2}]}`)
|
||||
})
|
||||
|
||||
images, _, err := client.Images.List()
|
||||
images, _, err := client.Images.List(nil)
|
||||
if err != nil {
|
||||
t.Errorf("Images.List returned error: %v", err)
|
||||
}
|
||||
|
@ -33,6 +33,53 @@ func TestImages_List(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestImages_ListImagesMultiplePages(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/v2/images", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
fmt.Fprint(w, `{"images": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/images/?page=2"}}}`)
|
||||
})
|
||||
|
||||
_, resp, err := client.Images.List(&ListOptions{Page: 2})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
checkCurrentPage(t, resp, 1)
|
||||
}
|
||||
|
||||
func TestImages_RetrievePageByNumber(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
jBlob := `
|
||||
{
|
||||
"images": [{"id":1},{"id":2}],
|
||||
"links":{
|
||||
"pages":{
|
||||
"next":"http://example.com/v2/images/?page=3",
|
||||
"prev":"http://example.com/v2/images/?page=1",
|
||||
"last":"http://example.com/v2/images/?page=3",
|
||||
"first":"http://example.com/v2/images/?page=1"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
mux.HandleFunc("/v2/images", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
fmt.Fprint(w, jBlob)
|
||||
})
|
||||
|
||||
opt := &ListOptions{Page: 2}
|
||||
_, resp, err := client.Images.List(opt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkCurrentPage(t, resp, 2)
|
||||
}
|
||||
|
||||
func TestImage_String(t *testing.T) {
|
||||
image := &Image{
|
||||
ID: 1,
|
||||
|
|
24
keys.go
24
keys.go
|
@ -8,7 +8,7 @@ const keysBasePath = "v2/account/keys"
|
|||
// endpoints of the Digital Ocean API
|
||||
// See: https://developers.digitalocean.com/#keys
|
||||
type KeysService interface {
|
||||
List() ([]Key, *Response, error)
|
||||
List(*ListOptions) ([]Key, *Response, error)
|
||||
GetByID(int) (*Key, *Response, error)
|
||||
GetByFingerprint(string) (*Key, *Response, error)
|
||||
Create(*KeyCreateRequest) (*Key, *Response, error)
|
||||
|
@ -31,7 +31,8 @@ type Key struct {
|
|||
}
|
||||
|
||||
type keysRoot struct {
|
||||
SSHKeys []Key `json:"ssh_keys"`
|
||||
SSHKeys []Key `json:"ssh_keys"`
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
type keyRoot struct {
|
||||
|
@ -49,19 +50,28 @@ type KeyCreateRequest struct {
|
|||
}
|
||||
|
||||
// List all keys
|
||||
func (s *KeysServiceOp) List() ([]Key, *Response, error) {
|
||||
req, err := s.client.NewRequest("GET", keysBasePath, nil)
|
||||
func (s *KeysServiceOp) List(opt *ListOptions) ([]Key, *Response, error) {
|
||||
path := keysBasePath
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
keys := new(keysRoot)
|
||||
resp, err := s.client.Do(req, keys)
|
||||
req, err := s.client.NewRequest("GET", path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(keysRoot)
|
||||
resp, err := s.client.Do(req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return keys.SSHKeys, resp, err
|
||||
return root.SSHKeys, resp, err
|
||||
}
|
||||
|
||||
// Performs a get given a path
|
||||
|
|
50
keys_test.go
50
keys_test.go
|
@ -20,10 +20,10 @@ func TestKeys_List(t *testing.T) {
|
|||
|
||||
mux.HandleFunc("/v2/account/keys", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
fmt.Fprint(w, `{"ssh_keys":[{"id":1},{"id":2}]} `)
|
||||
fmt.Fprint(w, `{"ssh_keys":[{"id":1},{"id":2}]}`)
|
||||
})
|
||||
|
||||
keys, _, err := client.Keys.List()
|
||||
keys, _, err := client.Keys.List(nil)
|
||||
if err != nil {
|
||||
t.Errorf("Keys.List returned error: %v", err)
|
||||
}
|
||||
|
@ -34,6 +34,52 @@ func TestKeys_List(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestKeys_ListKeysMultiplePages(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/v2/account/keys", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
fmt.Fprint(w, `{"droplets": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/account/keys/?page=2"}}}`)
|
||||
})
|
||||
|
||||
_, resp, err := client.Keys.List(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
checkCurrentPage(t, resp, 1)
|
||||
}
|
||||
|
||||
func TestKeys_RetrievePageByNumber(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
jBlob := `
|
||||
{
|
||||
"keys": [{"id":1},{"id":2}],
|
||||
"links":{
|
||||
"pages":{
|
||||
"next":"http://example.com/v2/account/keys/?page=3",
|
||||
"prev":"http://example.com/v2/account/keys/?page=1",
|
||||
"last":"http://example.com/v2/account/keys/?page=3",
|
||||
"first":"http://example.com/v2/account/keys/?page=1"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
mux.HandleFunc("/v2/account/keys", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
fmt.Fprint(w, jBlob)
|
||||
})
|
||||
|
||||
opt := &ListOptions{Page: 2}
|
||||
_, resp, err := client.Keys.List(opt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
checkCurrentPage(t, resp, 2)
|
||||
}
|
||||
|
||||
func TestKeys_GetByID(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Links manages links that are returned along with a List
|
||||
type Links struct {
|
||||
Pages *Pages `json:"pages,omitempty"`
|
||||
Actions []LinkAction `json:"actions,omitempty"`
|
||||
}
|
||||
|
||||
// Pages are pages specified in Links
|
||||
type Pages struct {
|
||||
First string `json:"first,omitempty"`
|
||||
Prev string `json:"prev,omitempty"`
|
||||
Last string `json:"last,omitempty"`
|
||||
Next string `json:"next,omitempty"`
|
||||
}
|
||||
|
||||
// LinkAction is a pointer to an action
|
||||
type LinkAction struct {
|
||||
ID int `json:"id,omitempty"`
|
||||
Rel string `json:"rel,omitempty"`
|
||||
HREF string `json:"href,omitempty"`
|
||||
}
|
||||
|
||||
// CurrentPage is current page of the list
|
||||
func (l *Links) CurrentPage() (int, error) {
|
||||
return l.Pages.current()
|
||||
}
|
||||
|
||||
func (p *Pages) current() (int, error) {
|
||||
switch {
|
||||
case p == nil:
|
||||
return 1, nil
|
||||
case p.Prev == "" && p.Next != "":
|
||||
return 1, nil
|
||||
case p.Prev != "":
|
||||
prevPage, err := pageForURL(p.Prev)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return prevPage + 1, nil
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// IsLastPage returns true if the current page is the last
|
||||
func (l *Links) IsLastPage() bool {
|
||||
if l == nil {
|
||||
return true
|
||||
}
|
||||
return l.Pages.isLast()
|
||||
}
|
||||
|
||||
func (p *Pages) isLast() bool {
|
||||
if p.Last == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func pageForURL(urlText string) (int, error) {
|
||||
u, err := url.ParseRequestURI(urlText)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
pageStr := u.Query().Get("page")
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (la *LinkAction) Get(client *Client) (*Action, *Response, error) {
|
||||
return client.Actions.Get(la.ID)
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
firstPageLinksJSONBlob = []byte(`{
|
||||
"links": {
|
||||
"pages": {
|
||||
"last": "https://api.digitalocean.com/v2/droplets/?page=3",
|
||||
"next": "https://api.digitalocean.com/v2/droplets/?page=2"
|
||||
}
|
||||
}
|
||||
}`)
|
||||
otherPageLinksJSONBlob = []byte(`{
|
||||
"links": {
|
||||
"pages": {
|
||||
"first": "https://api.digitalocean.com/v2/droplets/?page=1",
|
||||
"prev": "https://api.digitalocean.com/v2/droplets/?page=1",
|
||||
"last": "https://api.digitalocean.com/v2/droplets/?page=3",
|
||||
"next": "https://api.digitalocean.com/v2/droplets/?page=3"
|
||||
}
|
||||
}
|
||||
}`)
|
||||
lastPageLinksJSONBlob = []byte(`{
|
||||
"links": {
|
||||
"pages": {
|
||||
"first": "https://api.digitalocean.com/v2/droplets/?page=1",
|
||||
"prev": "https://api.digitalocean.com/v2/droplets/?page=2"
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
missingLinksJSONBlob = []byte(`{ }`)
|
||||
)
|
||||
|
||||
type godoList struct {
|
||||
Links Links `json:"links"`
|
||||
}
|
||||
|
||||
func loadLinksJSON(t *testing.T, j []byte) Links {
|
||||
var list godoList
|
||||
err := json.Unmarshal(j, &list)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return list.Links
|
||||
}
|
||||
|
||||
func TestLinks_ParseFirst(t *testing.T) {
|
||||
links := loadLinksJSON(t, firstPageLinksJSONBlob)
|
||||
_, err := links.CurrentPage()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := &Response{Links: &links}
|
||||
checkCurrentPage(t, r, 1)
|
||||
|
||||
if links.IsLastPage() {
|
||||
t.Fatalf("shouldn't be last page")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinks_ParseMiddle(t *testing.T) {
|
||||
links := loadLinksJSON(t, otherPageLinksJSONBlob)
|
||||
_, err := links.CurrentPage()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := &Response{Links: &links}
|
||||
checkCurrentPage(t, r, 2)
|
||||
|
||||
if links.IsLastPage() {
|
||||
t.Fatalf("shouldn't be last page")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinks_ParseLast(t *testing.T) {
|
||||
links := loadLinksJSON(t, lastPageLinksJSONBlob)
|
||||
_, err := links.CurrentPage()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := &Response{Links: &links}
|
||||
checkCurrentPage(t, r, 3)
|
||||
if !links.IsLastPage() {
|
||||
t.Fatalf("expected last page")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinks_ParseMissing(t *testing.T) {
|
||||
links := loadLinksJSON(t, missingLinksJSONBlob)
|
||||
_, err := links.CurrentPage()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := &Response{Links: &links}
|
||||
checkCurrentPage(t, r, 1)
|
||||
}
|
||||
|
||||
func TestLinks_ParseURL(t *testing.T) {
|
||||
type linkTest struct {
|
||||
name, url string
|
||||
expected int
|
||||
}
|
||||
|
||||
linkTests := []linkTest{
|
||||
{
|
||||
name: "prev",
|
||||
url: "https://api.digitalocean.com/v2/droplets/?page=1",
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "last",
|
||||
url: "https://api.digitalocean.com/v2/droplets/?page=5",
|
||||
expected: 5,
|
||||
},
|
||||
{
|
||||
name: "nexta",
|
||||
url: "https://api.digitalocean.com/v2/droplets/?page=2",
|
||||
expected: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, lT := range linkTests {
|
||||
p, err := pageForURL(lT.url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if p != lT.expected {
|
||||
t.Error("expected page for '%s' to be '%d', was '%d'",
|
||||
lT.url, lT.expected, p)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestLinks_ParseEmptyString(t *testing.T) {
|
||||
type linkTest struct {
|
||||
name, url string
|
||||
expected int
|
||||
}
|
||||
|
||||
linkTests := []linkTest{
|
||||
{
|
||||
name: "none",
|
||||
url: "http://example.com",
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "bad",
|
||||
url: "no url",
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
url: "",
|
||||
expected: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, lT := range linkTests {
|
||||
_, err := pageForURL(lT.url)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for test '%s', but received none", lT.name)
|
||||
}
|
||||
}
|
||||
}
|
18
regions.go
18
regions.go
|
@ -4,7 +4,7 @@ package godo
|
|||
// endpoints of the Digital Ocean API
|
||||
// See: https://developers.digitalocean.com/#regions
|
||||
type RegionsService interface {
|
||||
List() ([]Region, *Response, error)
|
||||
List(*ListOptions) ([]Region, *Response, error)
|
||||
}
|
||||
|
||||
// RegionsServiceOp handles communication with the region related methods of the
|
||||
|
@ -23,6 +23,7 @@ type Region struct {
|
|||
|
||||
type regionsRoot struct {
|
||||
Regions []Region
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
type regionRoot struct {
|
||||
|
@ -34,19 +35,26 @@ func (r Region) String() string {
|
|||
}
|
||||
|
||||
// List all regions
|
||||
func (s *RegionsServiceOp) List() ([]Region, *Response, error) {
|
||||
func (s *RegionsServiceOp) List(opt *ListOptions) ([]Region, *Response, error) {
|
||||
path := "v2/regions"
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest("GET", path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
regions := new(regionsRoot)
|
||||
resp, err := s.client.Do(req, regions)
|
||||
root := new(regionsRoot)
|
||||
resp, err := s.client.Do(req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return regions.Regions, resp, err
|
||||
return root.Regions, resp, err
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ func TestRegions_List(t *testing.T) {
|
|||
fmt.Fprint(w, `{"regions":[{"slug":"1"},{"slug":"2"}]}`)
|
||||
})
|
||||
|
||||
regions, _, err := client.Regions.List()
|
||||
regions, _, err := client.Regions.List(nil)
|
||||
if err != nil {
|
||||
t.Errorf("Regions.List returned error: %v", err)
|
||||
}
|
||||
|
@ -33,6 +33,54 @@ func TestRegions_List(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRegions_ListRegionsMultiplePages(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/v2/regions", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
fmt.Fprint(w, `{"regions": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/regions/?page=2"}}}`)
|
||||
})
|
||||
|
||||
_, resp, err := client.Regions.List(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkCurrentPage(t, resp, 1)
|
||||
}
|
||||
|
||||
func TestRegions_RetrievePageByNumber(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
jBlob := `
|
||||
{
|
||||
"regions": [{"id":1},{"id":2}],
|
||||
"links":{
|
||||
"pages":{
|
||||
"next":"http://example.com/v2/regions/?page=3",
|
||||
"prev":"http://example.com/v2/regions/?page=1",
|
||||
"last":"http://example.com/v2/regions/?page=3",
|
||||
"first":"http://example.com/v2/regions/?page=1"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
mux.HandleFunc("/v2/regions", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
fmt.Fprint(w, jBlob)
|
||||
})
|
||||
|
||||
opt := &ListOptions{Page: 2}
|
||||
_, resp, err := client.Regions.List(opt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkCurrentPage(t, resp, 2)
|
||||
}
|
||||
|
||||
func TestRegion_String(t *testing.T) {
|
||||
region := &Region{
|
||||
Slug: "region",
|
||||
|
|
18
sizes.go
18
sizes.go
|
@ -4,7 +4,7 @@ package godo
|
|||
// endpoints of the Digital Ocean API
|
||||
// See: https://developers.digitalocean.com/#sizes
|
||||
type SizesService interface {
|
||||
List() ([]Size, *Response, error)
|
||||
List(*ListOptions) ([]Size, *Response, error)
|
||||
}
|
||||
|
||||
// SizesServiceOp handles communication with the size related methods of the
|
||||
|
@ -30,22 +30,30 @@ func (s Size) String() string {
|
|||
|
||||
type sizesRoot struct {
|
||||
Sizes []Size
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
// List all images
|
||||
func (s *SizesServiceOp) List() ([]Size, *Response, error) {
|
||||
func (s *SizesServiceOp) List(opt *ListOptions) ([]Size, *Response, error) {
|
||||
path := "v2/sizes"
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest("GET", path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
sizes := new(sizesRoot)
|
||||
resp, err := s.client.Do(req, sizes)
|
||||
root := new(sizesRoot)
|
||||
resp, err := s.client.Do(req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return sizes.Sizes, resp, err
|
||||
return root.Sizes, resp, err
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ func TestSizes_List(t *testing.T) {
|
|||
fmt.Fprint(w, `{"sizes":[{"slug":"1"},{"slug":"2"}]}`)
|
||||
})
|
||||
|
||||
sizes, _, err := client.Sizes.List()
|
||||
sizes, _, err := client.Sizes.List(nil)
|
||||
if err != nil {
|
||||
t.Errorf("Sizes.List returned error: %v", err)
|
||||
}
|
||||
|
@ -33,6 +33,54 @@ func TestSizes_List(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSizes_ListSizesMultiplePages(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/v2/sizes", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
fmt.Fprint(w, `{"sizes": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/sizes/?page=2"}}}`)
|
||||
})
|
||||
|
||||
_, resp, err := client.Sizes.List(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkCurrentPage(t, resp, 1)
|
||||
}
|
||||
|
||||
func TestSizes_RetrievePageByNumber(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
jBlob := `
|
||||
{
|
||||
"sizes": [{"id":1},{"id":2}],
|
||||
"links":{
|
||||
"pages":{
|
||||
"next":"http://example.com/v2/sizes/?page=3",
|
||||
"prev":"http://example.com/v2/sizes/?page=1",
|
||||
"last":"http://example.com/v2/sizes/?page=3",
|
||||
"first":"http://example.com/v2/sizes/?page=1"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
mux.HandleFunc("/v2/sizes", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
fmt.Fprint(w, jBlob)
|
||||
})
|
||||
|
||||
opt := &ListOptions{Page: 2}
|
||||
_, resp, err := client.Sizes.List(opt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkCurrentPage(t, resp, 2)
|
||||
}
|
||||
|
||||
func TestSize_String(t *testing.T) {
|
||||
size := &Size{
|
||||
Slug: "slize",
|
||||
|
|
|
@ -2,9 +2,9 @@ package godo
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"io"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
var timestampType = reflect.TypeOf(Timestamp{})
|
||||
|
|
Loading…
Reference in New Issue