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 19 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
211 changes: 39 additions & 172 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,22 +19,32 @@ 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)
}
const aclEntriesPath = "/service/%s/acl/%s/entries"

// Swap implements the sortable interface.
func (s entriesByID) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
// 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.
Direction string
// Page is the current page.
Page int
// PerPage is the number of records per page.
PerPage int
// ServiceID is an alphanumeric string identifying the service (required).
ServiceID string
// Sort is the field on which to sort.
Sort string
}

// Less implements the sortable interface.
func (s entriesByID) Less(i, j int) bool {
return s[i].ID < s[j].ID
// 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))
}

// ListACLEntriesInput is the input parameter to ListACLEntries function.
Expand All @@ -58,25 +63,6 @@ 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
}

return m
}

// ListACLEntries retrieves all resources.
func (c *Client) ListACLEntries(i *ListACLEntriesInput) ([]*ACLEntry, error) {
if i.ACLID == "" {
Expand All @@ -85,142 +71,23 @@ func (c *Client) ListACLEntries(i *ListACLEntriesInput) ([]*ACLEntry, error) {
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
}
}

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])
p := c.GetACLEntries(&GetACLEntriesInput{
ACLID: i.ACLID,
Direction: i.Direction,
Page: i.Page,
PerPage: i.PerPage,
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)
}
// 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
results = append(results, data...)
}

sort.Stable(entriesByID(es))

return es, nil
return results, nil
}

// GetACLEntryInput is the input parameter to GetACLEntry function.
Expand Down Expand Up @@ -429,7 +296,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
27 changes: 18 additions & 9 deletions fastly/acl_entry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,22 +71,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 +174,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