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

feat: generic paginator #491

Merged
merged 21 commits into from
Nov 28, 2023
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
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ staticcheck: ## Runs the staticcheck linter.
@staticcheck ./...
.PHONY: staticcheck

nilaway: ## Run nilaway
@nilaway ./...

# Run semgrep checker.
.PHONY: semgrep
semgrep:
Expand Down
213 changes: 37 additions & 176 deletions fastly/acl_entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@ package fastly

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

"github.com/peterhellberg/link"
)

// ACLEntry represents a server response from the Fastly API.
Expand All @@ -24,26 +19,10 @@ type ACLEntry struct {
UpdatedAt *time.Time `mapstructure:"updated_at"`
}

// entriesByID is a sortable list of ACL entries.
type entriesByID []*ACLEntry

// Len implements the sortable interface.
func (s entriesByID) Len() int {
return len(s)
}

// Swap implements the sortable interface.
func (s entriesByID) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
const aclEntriesPath = "/service/%s/acl/%s/entries"

// Less implements the sortable interface.
func (s entriesByID) Less(i, j int) bool {
return s[i].ID < s[j].ID
}

// ListACLEntriesInput is the input parameter to ListACLEntries function.
type ListACLEntriesInput struct {
// GetACLEntriesInput is the input parameter to GetACLEntries function.
type GetACLEntriesInput struct {
// ACLID is an alphanumeric string identifying a ACL (required).
ACLID string
// Direction is the direction in which to sort results.
Expand All @@ -58,169 +37,51 @@ type ListACLEntriesInput struct {
Sort string
}

func (l *ListACLEntriesInput) formatFilters() map[string]string {
m := make(map[string]string)

if l.Direction != "" {
m["direction"] = l.Direction
}
if l.Page != 0 {
m["page"] = strconv.Itoa(l.Page)
}
if l.PerPage != 0 {
m["per_page"] = strconv.Itoa(l.PerPage)
}
if l.Sort != "" {
m["sort"] = l.Sort
}
// GetACLEntries returns a ListPaginator for paginating through the resources.
func (c *Client) GetACLEntries(i *GetACLEntriesInput) *ListPaginator[ACLEntry] {
return newPaginator[ACLEntry](c, &listInput{
Direction: i.Direction,
Sort: i.Sort,
Page: i.Page,
PerPage: i.PerPage,
}, fmt.Sprintf(aclEntriesPath, i.ServiceID, i.ACLID))
}

return m
// ListACLEntriesInput is the input parameter to ListACLEntries function.
type ListACLEntriesInput struct {
// ACLID is an alphanumeric string identifying a ACL (required).
ACLID string
// Direction is the direction in which to sort results.
Direction string
// ServiceID is an alphanumeric string identifying the service (required).
ServiceID string
// Sort is the field on which to sort.
Sort string
}

// ListACLEntries retrieves all resources.
// ListACLEntries retrieves all resources. Not suitable for large collections.
func (c *Client) ListACLEntries(i *ListACLEntriesInput) ([]*ACLEntry, error) {
if i.ACLID == "" {
return nil, ErrMissingACLID
}
if i.ServiceID == "" {
return nil, ErrMissingServiceID
}

path := fmt.Sprintf("/service/%s/acl/%s/entries", i.ServiceID, i.ACLID)

ro := new(RequestOptions)
ro.Params = i.formatFilters()

resp, err := c.Get(path, ro)
if err != nil {
return nil, err
}
defer resp.Body.Close()

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

sort.Stable(entriesByID(es))

return es, nil
}

// ListACLEntriesPaginator implements the PaginatorACLEntries interface.
type ListACLEntriesPaginator struct {
CurrentPage int
LastPage int
NextPage int

// Private
client *Client
consumed bool
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 paginator
func (c *Client) NewListACLEntriesPaginator(i *ListACLEntriesInput) PaginatorACLEntries {
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
}

// page is not specified, fetch from the beginning
if i.Page <= 0 && p.CurrentPage == 0 {
p.CurrentPage = 1
} else {
// page is specified, fetch from a given page
if !p.consumed {
p.CurrentPage = i.Page
} else {
p.CurrentPage = p.CurrentPage + 1
p := c.GetACLEntries(&GetACLEntriesInput{
ACLID: i.ACLID,
Direction: i.Direction,
ServiceID: i.ServiceID,
Sort: i.Sort,
})
var results []*ACLEntry
for p.HasNext() {
data, err := p.GetNext()
if err != nil {
return nil, fmt.Errorf("failed to get next page (remaining: %d): %s", p.Remaining(), err)
}
results = append(results, data...)
}

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
}
defer resp.Body.Close()

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
return results, nil
}

// GetACLEntryInput is the input parameter to GetACLEntry function.
Expand Down Expand Up @@ -429,7 +290,7 @@ func (c *Client) BatchModifyACLEntries(i *BatchModifyACLEntriesInput) error {
return ErrMaxExceededEntries
}

path := fmt.Sprintf("/service/%s/acl/%s/entries", i.ServiceID, i.ACLID)
path := fmt.Sprintf(aclEntriesPath, i.ServiceID, i.ACLID)
resp, err := c.PatchJSON(path, i, nil)
if err != nil {
return err
Expand Down
29 changes: 18 additions & 11 deletions fastly/acl_entry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ func TestClient_ACLEntries(t *testing.T) {
es, err = c.ListACLEntries(&ListACLEntriesInput{
ACLID: testACL.ID,
Direction: "descend",
Page: 1,
PerPage: 1,
ServiceID: testService.ID,
Sort: "created",
})
Expand All @@ -71,22 +69,31 @@ func TestClient_ACLEntries(t *testing.T) {

// List with paginator
var es2 []*ACLEntry
var paginator PaginatorACLEntries
var paginator *ListPaginator[ACLEntry]
record(t, fixtureBase+"list2", func(c *Client) {
paginator = c.NewListACLEntriesPaginator(&ListACLEntriesInput{
ServiceID: testService.ID,
paginator = c.GetACLEntries(&GetACLEntriesInput{
ACLID: testACL.ID,
Direction: "ascend",
PerPage: 50,
ServiceID: testService.ID,
Sort: "ip",
})
es2, err = paginator.GetNext()

for paginator.HasNext() {
data, err := paginator.GetNext()
if err != nil {
t.Errorf("Bad paginator (remaining: %d): %s", paginator.Remaining(), err)
return
}
es2 = append(es2, data...)
}
})
if err != nil {
t.Fatal(err)
}

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

if paginator.HasNext() {
t.Errorf("Bad paginator (remaining: %v)", paginator.Remaining())
}
Expand Down Expand Up @@ -165,14 +172,14 @@ func TestClient_ListACLEntries_validation(t *testing.T) {

_, err = testClient.ListACLEntries(&ListACLEntriesInput{})
if err != ErrMissingACLID {
t.Errorf("bad error: %s", err)
t.Errorf("bad ACL ID: %s", err)
}

_, err = testClient.ListACLEntries(&ListACLEntriesInput{
ACLID: "123",
})
if err != ErrMissingServiceID {
t.Errorf("bad error: %s", err)
t.Errorf("bad Service ID: %s", err)
}
}

Expand Down
5 changes: 1 addition & 4 deletions fastly/config_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,7 @@ func (c *Client) ListConfigStoreServices(i *ListConfigStoreServicesInput) ([]*Se
return nil, err
}

byName := servicesByName(ss)
sort.Sort(byName)

return byName, nil
return ss, nil
}

// UpdateConfigStoreInput is the input to UpdateConfigStore.
Expand Down
Loading
Loading