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 15 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
194 changes: 22 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,12 @@ 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
// ACLEntriesPath is exposed primarily for use by the generic Paginator.
// See ./paginator.go for details.
func ACLEntriesPath(serviceID, aclID string) string {
return fmt.Sprintf(aclEntriesPath, serviceID, aclID)
}
Integralist marked this conversation as resolved.
Show resolved Hide resolved

// ListACLEntriesInput is the input parameter to ListACLEntries function.
Expand All @@ -58,23 +43,14 @@ 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
// GetACLEntries returns a ListPaginator for paginating through the resources.
func (c *Client) GetACLEntries(i *ListACLEntriesInput) *ListPaginator[ACLEntry] {
return NewPaginator[ACLEntry](c, &ListInput{
Direction: i.Direction,
Sort: i.Sort,
Page: i.Page,
PerPage: i.PerPage,
}, ACLEntriesPath(i.ServiceID, i.ACLID))
Integralist marked this conversation as resolved.
Show resolved Hide resolved
}

// ListACLEntries retrieves all resources.
Expand All @@ -85,142 +61,16 @@ 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])
}
// 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 := c.GetACLEntries(i)
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...)
}

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 +279,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
60 changes: 49 additions & 11 deletions fastly/acl_entry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,24 +69,62 @@ func TestClient_ACLEntries(t *testing.T) {
t.Errorf("Bad entries: %v", es)
}

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

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

// List with GetN abstraction method for paginator construction
var es3 []*ACLEntry
record(t, fixtureBase+"list3", func(c *Client) {
listACLEntriesInput := &ListACLEntriesInput{
ACLID: testACL.ID,
Direction: "ascend",
PerPage: 50,
ServiceID: testService.ID,
Sort: "ip",
}
paginator = c.GetACLEntries(listACLEntriesInput)

for paginator.HasNext() {
data, err := paginator.GetNext()
if err != nil {
t.Errorf("Bad paginator (remaining: %d): %s", paginator.Remaining(), err)
return
}
es3 = append(es3, data...)
}
})
if err != nil {
t.Fatal(err)
}
if len(es3) != 1 {
t.Errorf("Bad entries: %v", es3)
}
if paginator.HasNext() {
t.Errorf("Bad paginator (remaining: %v)", paginator.Remaining())
}
Expand Down Expand Up @@ -165,14 +203,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