Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Link header pagination #321

Merged
merged 8 commits into from
Jan 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions fastly/acl_entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package fastly

import (
"fmt"
"net/url"
"sort"
"strconv"
"time"

"github.com/peterhellberg/link"
)

type ACLEntry struct {
Expand Down Expand Up @@ -34,6 +38,10 @@ func (s entriesById) Less(i, j int) bool {
type ListACLEntriesInput struct {
ServiceID string
ACLID string
Direction string
PerPage int
Page int
Sort string
}

// ListACLEntries return a list of entries for an ACL
Expand Down Expand Up @@ -63,6 +71,113 @@ func (c *Client) ListACLEntries(i *ListACLEntriesInput) ([]*ACLEntry, error) {
return es, nil
}

type ListAclEntriesPaginator struct {
consumed bool
CurrentPage int
NextPage int
LastPage int
client *Client
options *ListACLEntriesInput
}

// HasNext returns a boolean indicating whether more pages are available
func (p *ListAclEntriesPaginator) HasNext() bool {
return !p.consumed || p.Remaining() != 0
}

// Remaining returns the remaining page count
func (p *ListAclEntriesPaginator) Remaining() int {
if p.LastPage == 0 {
return 0
}
return p.LastPage - p.CurrentPage
}

// GetNext retrieves data in the next page
func (p *ListAclEntriesPaginator) GetNext() ([]*ACLEntry, error) {
return p.client.listACLEntriesWithPage(p.options, p)
}

// NewListACLEntriesPaginator returns a new ListAclEntriesPaginator
func (c *Client) NewListACLEntriesPaginator(i *ListACLEntriesInput) *ListAclEntriesPaginator {
return &ListAclEntriesPaginator{
client: c,
options: i,
}
}

// listACLEntriesWithPage return a list of entries for an ACL of a given page
func (c *Client) listACLEntriesWithPage(i *ListACLEntriesInput, p *ListAclEntriesPaginator) ([]*ACLEntry, error) {

if i.ServiceID == "" {
return nil, ErrMissingServiceID
}

if i.ACLID == "" {
return nil, ErrMissingACLID
}

var perPage int
const maxPerPage = 100
if i.PerPage <= 0 {
perPage = maxPerPage
} else {
perPage = i.PerPage
}

if i.Page <= 0 && p.CurrentPage == 0 {
p.CurrentPage = 1
} else {
p.CurrentPage = p.CurrentPage + 1
}

path := fmt.Sprintf("/service/%s/acl/%s/entries", i.ServiceID, i.ACLID)
requestOptions := &RequestOptions{
Params: map[string]string{
"per_page": strconv.Itoa(perPage),
"page": strconv.Itoa(p.CurrentPage),
},
}

if i.Direction != "" {
requestOptions.Params["direction"] = i.Direction
}
if i.Sort != "" {
requestOptions.Params["sort"] = i.Sort
}

resp, err := c.Get(path, requestOptions)
if err != nil {
return nil, err
}

for _, l := range link.ParseResponse(resp) {
// indicates the Link response header contained the next page instruction
if l.Rel == "next" {
u, _ := url.Parse(l.URI)
query := u.Query()
p.NextPage, _ = strconv.Atoi(query["page"][0])
}
// indicates the Link response header contained the last page instruction
if l.Rel == "last" {
u, _ := url.Parse(l.URI)
query := u.Query()
p.LastPage, _ = strconv.Atoi(query["page"][0])
}
}

p.consumed = true

var es []*ACLEntry
if err := decodeBodyMap(resp.Body, &es); err != nil {
return nil, err
}

sort.Stable(entriesById(es))

return es, nil
}

// GetACLEntryInput is the input parameter to GetACLEntry function.
type GetACLEntryInput struct {
ServiceID string
Expand Down
22 changes: 22 additions & 0 deletions fastly/acl_entry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,28 @@ func TestClient_ACLEntries(t *testing.T) {
t.Errorf("Bad entries: %v", es)
}

// List with paginator
var es2 []*ACLEntry
var paginator *ListAclEntriesPaginator
record(t, fixtureBase+"list2", func(c *Client) {
paginator = c.NewListACLEntriesPaginator(&ListACLEntriesInput{
ServiceID: testService.ID,
ACLID: testACL.ID,
})
es2, err = paginator.GetNext()
})
if err != nil {
t.Fatal(err)
}

if len(es2) != 1 {
t.Errorf("Bad entries: %v", es)
}

if paginator.HasNext() {
t.Errorf("Bad paginator (remaining: %v)", paginator.Remaining())
}

// Get
var ne *ACLEntry
record(t, fixtureBase+"get", func(c *Client) {
Expand Down
115 changes: 113 additions & 2 deletions fastly/dictionary_item.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import (
"fmt"
"net/url"
"sort"
"strconv"
"time"

"github.com/peterhellberg/link"
)

// DictionaryItem represents a dictionary item response from the Fastly API.
Expand Down Expand Up @@ -36,10 +39,13 @@ type ListDictionaryItemsInput struct {

// DictionaryID is the ID of the dictionary to retrieve items for (required).
DictionaryID string
Direction string
PerPage int
Page int
Sort string
}

// ListDictionaryItems returns the list of dictionary items for the
// configuration version.
// ListDictionaryItems returns a list of items for a dictionary
func (c *Client) ListDictionaryItems(i *ListDictionaryItemsInput) ([]*DictionaryItem, error) {
if i.ServiceID == "" {
return nil, ErrMissingServiceID
Expand All @@ -63,6 +69,111 @@ func (c *Client) ListDictionaryItems(i *ListDictionaryItemsInput) ([]*Dictionary
return bs, nil
}

type ListDictionaryItemsPaginator struct {
consumed bool
CurrentPage int
NextPage int
LastPage int
client *Client
options *ListDictionaryItemsInput
}

// HasNext returns a boolean indicating whether more pages are available
func (p *ListDictionaryItemsPaginator) HasNext() bool {
return !p.consumed || p.Remaining() != 0
}

// Remaining returns the remaining page count
func (p *ListDictionaryItemsPaginator) Remaining() int {
if p.LastPage == 0 {
return 0
}
return p.LastPage - p.CurrentPage
}

// GetNext retrieves data in the next page
func (p *ListDictionaryItemsPaginator) GetNext() ([]*DictionaryItem, error) {
return p.client.listDictionaryItemsWithPage(p.options, p)
}

// NewListDictionaryItemsPaginator returns a new ListDictionaryItemsPaginator
func (c *Client) NewListDictionaryItemsPaginator(i *ListDictionaryItemsInput) *ListDictionaryItemsPaginator {
return &ListDictionaryItemsPaginator{
client: c,
options: i,
}
}

// listDictionaryItemsWithPage returns a list of items for a dictionary of a given page
func (c *Client) listDictionaryItemsWithPage(i *ListDictionaryItemsInput, p *ListDictionaryItemsPaginator) ([]*DictionaryItem, error) {
if i.ServiceID == "" {
return nil, ErrMissingServiceID
}

if i.DictionaryID == "" {
return nil, ErrMissingDictionaryID
}

var perPage int
const maxPerPage = 100
if i.PerPage <= 0 {
perPage = maxPerPage
} else {
perPage = i.PerPage
}

if i.Page <= 0 && p.CurrentPage == 0 {
p.CurrentPage = 1
} else {
p.CurrentPage = p.CurrentPage + 1
}

path := fmt.Sprintf("/service/%s/dictionary/%s/items", i.ServiceID, i.DictionaryID)
requestOptions := &RequestOptions{
Params: map[string]string{
"per_page": strconv.Itoa(perPage),
"page": strconv.Itoa(p.CurrentPage),
},
}

if i.Direction != "" {
requestOptions.Params["direction"] = i.Direction
}
if i.Sort != "" {
requestOptions.Params["sort"] = i.Sort
}

resp, err := c.Get(path, requestOptions)
if err != nil {
return nil, err
}

for _, l := range link.ParseResponse(resp) {
// indicates the Link response header contained the next page instruction
if l.Rel == "next" {
u, _ := url.Parse(l.URI)
query := u.Query()
p.NextPage, _ = strconv.Atoi(query["page"][0])
}
// indicates the Link response header contained the last page instruction
if l.Rel == "last" {
u, _ := url.Parse(l.URI)
query := u.Query()
p.LastPage, _ = strconv.Atoi(query["page"][0])
}
}

p.consumed = true

var bs []*DictionaryItem
if err := decodeBodyMap(resp.Body, &bs); err != nil {
return nil, err
}
sort.Stable(dictionaryItemsByKey(bs))

return bs, nil
}

// CreateDictionaryItemInput is used as input to the CreateDictionaryItem function.
type CreateDictionaryItemInput struct {
// ServiceID is the ID of the service (required).
Expand Down
22 changes: 22 additions & 0 deletions fastly/dictionary_item_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,28 @@ func TestClient_DictionaryItems(t *testing.T) {
t.Errorf("bad dictionary items: %v", dictionaryItems)
}

// List with paginator
var dictionaryItems2 []*DictionaryItem
var paginator *ListDictionaryItemsPaginator
record(t, fixtureBase+"list2", func(c *Client) {
paginator = c.NewListDictionaryItemsPaginator(&ListDictionaryItemsInput{
ServiceID: testService.ID,
DictionaryID: testDictionary.ID,
})
dictionaryItems2, err = paginator.GetNext()
})
if err != nil {
t.Fatal(err)
}

if len(dictionaryItems2) != 1 {
t.Errorf("Bad items: %v", dictionaryItems2)
}

if paginator.HasNext() {
t.Errorf("Bad paginator (remaining: %v)", paginator.Remaining())
}

// Get
var retrievedDictionaryItem *DictionaryItem
record(t, fixtureBase+"get", func(c *Client) {
Expand Down
34 changes: 34 additions & 0 deletions fastly/example_acl_entry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package fastly_test

import (
"fmt"
"log"

"github.com/fastly/go-fastly/v5/fastly"
)

func ExampleClient_NewListACLEntriesPaginator() {
client, err := fastly.NewClient("your_api_token")
if err != nil {
log.Fatal(err)
}

paginator := client.NewListACLEntriesPaginator(
&fastly.ListACLEntriesInput{
ServiceID: "your_service_id",
ACLID: "your_acl_id",
PerPage: 50,
},
)

var es []*fastly.ACLEntry
for paginator.HasNext() {
data, err := paginator.GetNext()
if err != nil {
break
}
es = append(es, data...)
}

fmt.Printf("retrieved %d ACL entries\n", len(es))
}
Loading