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:
bryanl 2014-09-06 17:56:23 -04:00
parent 6a5163c77e
commit 96accbf5e9
20 changed files with 989 additions and 246 deletions

View File

@ -1,11 +1,16 @@
# GODO # Godo
Godo is a Go client library for accessing the DigitalOcean V2 API. 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 ## Usage
```go ```go
import "github.com/digitaloceancloud/godo" import "github.com/digitalocean/godo"
``` ```
Create a new DigitalOcean client, then use the exposed services to Create a new DigitalOcean client, then use the exposed services to
@ -32,20 +37,6 @@ client := godo.NewClient(t.Client())
## Examples ## 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: To create a new Droplet:
@ -53,16 +44,57 @@ To create a new Droplet:
dropletName := "super-cool-droplet" dropletName := "super-cool-droplet"
createRequest := &godo.DropletCreateRequest{ createRequest := &godo.DropletCreateRequest{
Name: godo.String(dropletName), Name: godo.String(dropletName),
Region: godo.String("nyc2"), Region: godo.String("nyc2"),
Size: godo.String("512mb"), Size: godo.String("512mb"),
Image: godo.Int(3240036), // ubuntu 14.04 64bit Image: godo.Int(3240036), // ubuntu 14.04 64bit
} }
newDroplet, _, err := client.Droplet.Create(createRequest) newDroplet, _, err := client.Droplet.Create(createRequest)
if err != nil { if err != nil {
fmt.Printf("Something bad happened: %s\n\n", err) fmt.Printf("Something bad happened: %s\n\n", err)
return 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
}
```

View File

@ -15,7 +15,7 @@ const (
// ActionsService handles communction with action related methods of the // ActionsService handles communction with action related methods of the
// DigitalOcean API: https://developers.digitalocean.com/#actions // DigitalOcean API: https://developers.digitalocean.com/#actions
type ActionsService interface { type ActionsService interface {
List() ([]Action, *Response, error) List(*ListOptions) ([]Action, *Response, error)
Get(int) (*Action, *Response, error) Get(int) (*Action, *Response, error)
} }
@ -27,6 +27,7 @@ type ActionsServiceOp struct {
type actionsRoot struct { type actionsRoot struct {
Actions []Action `json:"actions"` Actions []Action `json:"actions"`
Links *Links `json:"links"`
} }
type actionRoot struct { type actionRoot struct {
@ -45,8 +46,12 @@ type Action struct {
} }
// List all actions // List all actions
func (s *ActionsServiceOp) List() ([]Action, *Response, error) { func (s *ActionsServiceOp) List(opt *ListOptions) ([]Action, *Response, error) {
path := actionsBasePath path := actionsBasePath
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest("GET", path, nil)
if err != nil { if err != nil {
@ -58,6 +63,9 @@ func (s *ActionsServiceOp) List() ([]Action, *Response, error) {
if err != nil { if err != nil {
return nil, resp, err return nil, resp, err
} }
if l := root.Links; l != nil {
resp.Links = l
}
return root.Actions, resp, err return root.Actions, resp, err
} }

View File

@ -22,7 +22,7 @@ func TestAction_List(t *testing.T) {
testMethod(t, r, "GET") testMethod(t, r, "GET")
}) })
actions, _, err := client.Actions.List() actions, _, err := client.Actions.List(nil)
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) 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) { func TestAction_Get(t *testing.T) {
setup() setup()
defer teardown() defer teardown()

View File

@ -8,7 +8,12 @@ const domainsBasePath = "v2/domains"
// See: https://developers.digitalocean.com/#domains and // See: https://developers.digitalocean.com/#domains and
// https://developers.digitalocean.com/#domain-records // https://developers.digitalocean.com/#domain-records
type DomainsService interface { 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) Record(string, int) (*DomainRecord, *Response, error)
DeleteRecord(string, int) (*Response, error) DeleteRecord(string, int) (*Response, error)
EditRecord(string, int, *DomainRecordEditRequest) (*DomainRecord, *Response, error) EditRecord(string, int, *DomainRecordEditRequest) (*DomainRecord, *Response, error)
@ -21,6 +26,29 @@ type DomainsServiceOp struct {
client *Client 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 // DomainRecordRoot is the root of an individual Domain Record response
type DomainRecordRoot struct { type DomainRecordRoot struct {
DomainRecord *DomainRecord `json:"domain_record"` DomainRecord *DomainRecord `json:"domain_record"`
@ -29,6 +57,7 @@ type DomainRecordRoot struct {
// DomainRecordsRoot is the root of a group of Domain Record responses // DomainRecordsRoot is the root of a group of Domain Record responses
type DomainRecordsRoot struct { type DomainRecordsRoot struct {
DomainRecords []DomainRecord `json:"domain_records"` DomainRecords []DomainRecord `json:"domain_records"`
Links *Links `json:"links"`
} }
// DomainRecord represents a DigitalOcean DomainRecord // DomainRecord represents a DigitalOcean DomainRecord
@ -42,16 +71,6 @@ type DomainRecord struct {
Weight int `json:"weight,omitempty"` 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. // DomainRecordEditRequest represents a request to update a domain record.
type DomainRecordEditRequest struct { type DomainRecordEditRequest struct {
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
@ -62,13 +81,97 @@ type DomainRecordEditRequest struct {
Weight int `json:"weight,omitempty"` 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. // Converts a DomainRecordEditRequest to a string.
func (d DomainRecordEditRequest) String() string { func (d DomainRecordEditRequest) String() string {
return Stringify(d) return Stringify(d)
} }
// Records returns a slice of DomainRecords for a domain // 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 := fmt.Sprintf("%s/%s/records", domainsBasePath, domain)
path, err := addOptions(path, opt) path, err := addOptions(path, opt)
if err != nil { if err != nil {
@ -80,13 +183,16 @@ func (s *DomainsServiceOp) Records(domain string, opt *DomainRecordsOptions) ([]
return nil, nil, err return nil, nil, err
} }
records := new(DomainRecordsRoot) root := new(DomainRecordsRoot)
resp, err := s.client.Do(req, records) resp, err := s.client.Do(req, root)
if err != nil { if err != nil {
return nil, resp, err 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 // Record returns the record id from a domain

View File

@ -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) { func TestDomains_AllRecordsForDomainName(t *testing.T) {
setup() setup()
defer teardown() defer teardown()
@ -47,7 +190,7 @@ func TestDomains_AllRecordsForDomainName_PerPage(t *testing.T) {
fmt.Fprint(w, `{"domain_records":[{"id":1},{"id":2}]}`) 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) records, _, err := client.Domains.Records("example.com", dro)
if err != nil { if err != nil {
t.Errorf("Domains.List returned error: %v", err) t.Errorf("Domains.List returned error: %v", err)

View File

@ -8,7 +8,7 @@ const dropletBasePath = "v2/droplets"
// endpoints of the Digital Ocean API // endpoints of the Digital Ocean API
// See: https://developers.digitalocean.com/#droplets // See: https://developers.digitalocean.com/#droplets
type DropletsService interface { type DropletsService interface {
List() ([]Droplet, *Response, error) List(*ListOptions) ([]Droplet, *Response, error)
Get(int) (*DropletRoot, *Response, error) Get(int) (*DropletRoot, *Response, error)
Create(*DropletCreateRequest) (*DropletRoot, *Response, error) Create(*DropletCreateRequest) (*DropletRoot, *Response, error)
Delete(int) (*Response, error) Delete(int) (*Response, error)
@ -52,6 +52,7 @@ type DropletRoot struct {
type dropletsRoot struct { type dropletsRoot struct {
Droplets []Droplet `json:"droplets"` Droplets []Droplet `json:"droplets"`
Links *Links `json:"links"`
} }
// DropletCreateRequest represents a request to create a droplet. // DropletCreateRequest represents a request to create a droplet.
@ -85,45 +86,29 @@ func (n Network) String() string {
return Stringify(n) 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 // List all droplets
func (s *DropletsServiceOp) List() ([]Droplet, *Response, error) { func (s *DropletsServiceOp) List(opt *ListOptions) ([]Droplet, *Response, error) {
path := dropletBasePath path := dropletBasePath
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest("GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
droplets := new(dropletsRoot) root := new(dropletsRoot)
resp, err := s.client.Do(req, droplets) resp, err := s.client.Do(req, root)
if err != nil { if err != nil {
return nil, resp, err 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 // Get individual droplet

View File

@ -23,7 +23,7 @@ func TestDroplets_ListDroplets(t *testing.T) {
fmt.Fprint(w, `{"droplets": [{"id":1},{"id":2}]}`) fmt.Fprint(w, `{"droplets": [{"id":1},{"id":2}]}`)
}) })
droplets, _, err := client.Droplet.List() droplets, _, err := client.Droplets.List(nil)
if err != nil { if err != nil {
t.Errorf("Droplets.List returned error: %v", err) 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) { func TestDroplets_GetDroplet(t *testing.T) {
setup() setup()
defer teardown() defer teardown()
@ -43,7 +107,7 @@ func TestDroplets_GetDroplet(t *testing.T) {
fmt.Fprint(w, `{"droplet":{"id":12345}}`) fmt.Fprint(w, `{"droplet":{"id":12345}}`)
}) })
droplets, _, err := client.Droplet.Get(12345) droplets, _, err := client.Droplets.Get(12345)
if err != nil { if err != nil {
t.Errorf("Droplet.Get returned error: %v", err) t.Errorf("Droplet.Get returned error: %v", err)
} }
@ -77,7 +141,7 @@ func TestDroplets_Create(t *testing.T) {
fmt.Fprintf(w, `{"droplet":{"id":1}}`) fmt.Fprintf(w, `{"droplet":{"id":1}}`)
}) })
droplet, _, err := client.Droplet.Create(createRequest) droplet, _, err := client.Droplets.Create(createRequest)
if err != nil { if err != nil {
t.Errorf("Droplets.Create returned error: %v", err) t.Errorf("Droplets.Create returned error: %v", err)
} }
@ -96,34 +160,12 @@ func TestDroplets_Destroy(t *testing.T) {
testMethod(t, r, "DELETE") testMethod(t, r, "DELETE")
}) })
_, err := client.Droplet.Delete(12345) _, err := client.Droplets.Delete(12345)
if err != nil { if err != nil {
t.Errorf("Droplet.Delete returned error: %v", err) 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) { func TestNetwork_String(t *testing.T) {
network := &Network{ network := &Network{
IPAddress: "192.168.1.2", IPAddress: "192.168.1.2",

59
godo.go
View File

@ -45,7 +45,7 @@ type Client struct {
// Services used for communicating with the API // Services used for communicating with the API
Actions ActionsService Actions ActionsService
Domains DomainsService Domains DomainsService
Droplet DropletsService Droplets DropletsService
DropletActions DropletActionsService DropletActions DropletActionsService
Images ImagesService Images ImagesService
ImageActions ImageActionsService ImageActions ImageActionsService
@ -68,15 +68,9 @@ type ListOptions struct {
type Response struct { type Response struct {
*http.Response *http.Response
// These fields provide the page values for paginating through a set of // Links that were returned with the response. These are parsed from
// results. Any or all of these may be set to the zero value for // request body and not the header.
// responses that are not part of a paginated set, or for which there Links *Links
// are no additional pages.
NextPage string
PrevPage string
FirstPage string
LastPage string
// Monitoring URI // Monitoring URI
Monitor string Monitor string
@ -137,7 +131,7 @@ func NewClient(httpClient *http.Client) *Client {
c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent} c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent}
c.Actions = &ActionsServiceOp{client: c} c.Actions = &ActionsServiceOp{client: c}
c.Domains = &DomainsServiceOp{client: c} c.Domains = &DomainsServiceOp{client: c}
c.Droplet = &DropletsServiceOp{client: c} c.Droplets = &DropletsServiceOp{client: c}
c.DropletActions = &DropletActionsServiceOp{client: c} c.DropletActions = &DropletActionsServiceOp{client: c}
c.Images = &ImagesServiceOp{client: c} c.Images = &ImagesServiceOp{client: c}
c.ImageActions = &ImageActionsServiceOp{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 // newResponse creates a new Response for the provided http.Response
func newResponse(r *http.Response) *Response { func newResponse(r *http.Response) *Response {
response := Response{Response: r} response := Response{Response: r}
response.populatePageValues()
response.populateRate() response.populateRate()
response.populateMonitor()
return &response 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) { func (r *Response) links() (map[string]headerLink.Link, error) {
if linkText, ok := r.Response.Header["Link"]; ok { if linkText, ok := r.Response.Header["Link"]; ok {
links, err := headerLink.Parse(linkText[0]) links, err := headerLink.Parse(linkText[0])

View File

@ -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 { func Implements(interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) bool {
interfaceType := reflect.TypeOf(interfaceObject).Elem() interfaceType := reflect.TypeOf(interfaceObject).Elem()
if !reflect.TypeOf(object).Implements(interfaceType) { if !reflect.TypeOf(object).Implements(interfaceType) {
@ -376,3 +308,15 @@ func Implements(interfaceObject interface{}, object interface{}, msgAndArgs ...i
return true 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)
}
}

View File

@ -4,7 +4,7 @@ package godo
// endpoints of the Digital Ocean API // endpoints of the Digital Ocean API
// See: https://developers.digitalocean.com/#images // See: https://developers.digitalocean.com/#images
type ImagesService interface { type ImagesService interface {
List() ([]Image, *Response, error) List(*ListOptions) ([]Image, *Response, error)
} }
// ImagesServiceOp handles communication with the image related methods of the // ImagesServiceOp handles communication with the image related methods of the
@ -29,6 +29,7 @@ type imageRoot struct {
type imagesRoot struct { type imagesRoot struct {
Images []Image Images []Image
Links *Links `json:"links"`
} }
func (i Image) String() string { func (i Image) String() string {
@ -36,19 +37,26 @@ func (i Image) String() string {
} }
// List all sizes // List all sizes
func (s *ImagesServiceOp) List() ([]Image, *Response, error) { func (s *ImagesServiceOp) List(opt *ListOptions) ([]Image, *Response, error) {
path := "v2/images" path := "v2/images"
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest("GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
images := new(imagesRoot) root := new(imagesRoot)
resp, err := s.client.Do(req, images) resp, err := s.client.Do(req, root)
if err != nil { if err != nil {
return nil, resp, err return nil, resp, err
} }
if l := root.Links; l != nil {
resp.Links = l
}
return images.Images, resp, err return root.Images, resp, err
} }

View File

@ -22,7 +22,7 @@ func TestImages_List(t *testing.T) {
fmt.Fprint(w, `{"images":[{"id":1},{"id":2}]}`) fmt.Fprint(w, `{"images":[{"id":1},{"id":2}]}`)
}) })
images, _, err := client.Images.List() images, _, err := client.Images.List(nil)
if err != nil { if err != nil {
t.Errorf("Images.List returned error: %v", err) 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) { func TestImage_String(t *testing.T) {
image := &Image{ image := &Image{
ID: 1, ID: 1,

24
keys.go
View File

@ -8,7 +8,7 @@ const keysBasePath = "v2/account/keys"
// endpoints of the Digital Ocean API // endpoints of the Digital Ocean API
// See: https://developers.digitalocean.com/#keys // See: https://developers.digitalocean.com/#keys
type KeysService interface { type KeysService interface {
List() ([]Key, *Response, error) List(*ListOptions) ([]Key, *Response, error)
GetByID(int) (*Key, *Response, error) GetByID(int) (*Key, *Response, error)
GetByFingerprint(string) (*Key, *Response, error) GetByFingerprint(string) (*Key, *Response, error)
Create(*KeyCreateRequest) (*Key, *Response, error) Create(*KeyCreateRequest) (*Key, *Response, error)
@ -31,7 +31,8 @@ type Key struct {
} }
type keysRoot struct { type keysRoot struct {
SSHKeys []Key `json:"ssh_keys"` SSHKeys []Key `json:"ssh_keys"`
Links *Links `json:"links"`
} }
type keyRoot struct { type keyRoot struct {
@ -49,19 +50,28 @@ type KeyCreateRequest struct {
} }
// List all keys // List all keys
func (s *KeysServiceOp) List() ([]Key, *Response, error) { func (s *KeysServiceOp) List(opt *ListOptions) ([]Key, *Response, error) {
req, err := s.client.NewRequest("GET", keysBasePath, nil) path := keysBasePath
path, err := addOptions(path, opt)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
keys := new(keysRoot) req, err := s.client.NewRequest("GET", path, nil)
resp, err := s.client.Do(req, keys) if err != nil {
return nil, nil, err
}
root := new(keysRoot)
resp, err := s.client.Do(req, root)
if err != nil { if err != nil {
return nil, resp, err 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 // Performs a get given a path

View File

@ -20,10 +20,10 @@ func TestKeys_List(t *testing.T) {
mux.HandleFunc("/v2/account/keys", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/v2/account/keys", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET") 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 { if err != nil {
t.Errorf("Keys.List returned error: %v", err) 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) { func TestKeys_GetByID(t *testing.T) {
setup() setup()
defer teardown() defer teardown()

85
links.go Normal file
View File

@ -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)
}

176
links_test.go Normal file
View File

@ -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)
}
}
}

View File

@ -4,7 +4,7 @@ package godo
// endpoints of the Digital Ocean API // endpoints of the Digital Ocean API
// See: https://developers.digitalocean.com/#regions // See: https://developers.digitalocean.com/#regions
type RegionsService interface { type RegionsService interface {
List() ([]Region, *Response, error) List(*ListOptions) ([]Region, *Response, error)
} }
// RegionsServiceOp handles communication with the region related methods of the // RegionsServiceOp handles communication with the region related methods of the
@ -23,6 +23,7 @@ type Region struct {
type regionsRoot struct { type regionsRoot struct {
Regions []Region Regions []Region
Links *Links `json:"links"`
} }
type regionRoot struct { type regionRoot struct {
@ -34,19 +35,26 @@ func (r Region) String() string {
} }
// List all regions // List all regions
func (s *RegionsServiceOp) List() ([]Region, *Response, error) { func (s *RegionsServiceOp) List(opt *ListOptions) ([]Region, *Response, error) {
path := "v2/regions" path := "v2/regions"
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest("GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
regions := new(regionsRoot) root := new(regionsRoot)
resp, err := s.client.Do(req, regions) resp, err := s.client.Do(req, root)
if err != nil { if err != nil {
return nil, resp, err return nil, resp, err
} }
if l := root.Links; l != nil {
resp.Links = l
}
return regions.Regions, resp, err return root.Regions, resp, err
} }

View File

@ -22,7 +22,7 @@ func TestRegions_List(t *testing.T) {
fmt.Fprint(w, `{"regions":[{"slug":"1"},{"slug":"2"}]}`) fmt.Fprint(w, `{"regions":[{"slug":"1"},{"slug":"2"}]}`)
}) })
regions, _, err := client.Regions.List() regions, _, err := client.Regions.List(nil)
if err != nil { if err != nil {
t.Errorf("Regions.List returned error: %v", err) 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) { func TestRegion_String(t *testing.T) {
region := &Region{ region := &Region{
Slug: "region", Slug: "region",

View File

@ -4,7 +4,7 @@ package godo
// endpoints of the Digital Ocean API // endpoints of the Digital Ocean API
// See: https://developers.digitalocean.com/#sizes // See: https://developers.digitalocean.com/#sizes
type SizesService interface { type SizesService interface {
List() ([]Size, *Response, error) List(*ListOptions) ([]Size, *Response, error)
} }
// SizesServiceOp handles communication with the size related methods of the // SizesServiceOp handles communication with the size related methods of the
@ -30,22 +30,30 @@ func (s Size) String() string {
type sizesRoot struct { type sizesRoot struct {
Sizes []Size Sizes []Size
Links *Links `json:"links"`
} }
// List all images // List all images
func (s *SizesServiceOp) List() ([]Size, *Response, error) { func (s *SizesServiceOp) List(opt *ListOptions) ([]Size, *Response, error) {
path := "v2/sizes" path := "v2/sizes"
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest("GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
sizes := new(sizesRoot) root := new(sizesRoot)
resp, err := s.client.Do(req, sizes) resp, err := s.client.Do(req, root)
if err != nil { if err != nil {
return nil, resp, err return nil, resp, err
} }
if l := root.Links; l != nil {
resp.Links = l
}
return sizes.Sizes, resp, err return root.Sizes, resp, err
} }

View File

@ -22,7 +22,7 @@ func TestSizes_List(t *testing.T) {
fmt.Fprint(w, `{"sizes":[{"slug":"1"},{"slug":"2"}]}`) fmt.Fprint(w, `{"sizes":[{"slug":"1"},{"slug":"2"}]}`)
}) })
sizes, _, err := client.Sizes.List() sizes, _, err := client.Sizes.List(nil)
if err != nil { if err != nil {
t.Errorf("Sizes.List returned error: %v", err) 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) { func TestSize_String(t *testing.T) {
size := &Size{ size := &Size{
Slug: "slize", Slug: "slize",

View File

@ -2,9 +2,9 @@ package godo
import ( import (
"bytes" "bytes"
"reflect"
"io"
"fmt" "fmt"
"io"
"reflect"
) )
var timestampType = reflect.TypeOf(Timestamp{}) var timestampType = reflect.TypeOf(Timestamp{})