Testing out a new pagination idiom.
diff --git a/collections.go b/collections.go
index 443a000..38078f0 100644
--- a/collections.go
+++ b/collections.go
@@ -1,65 +1,87 @@
package gophercloud
-import (
- "errors"
-
- "github.com/racker/perigee"
-)
+import "errors"
var (
// ErrPageNotAvailable is returned from a Pager when a next or previous page is requested, but does not exist.
ErrPageNotAvailable = errors.New("The requested Collection page does not exist.")
)
-// Collection describes the minimum functionality that any collection resource must implement to be able to use
-// the global paging and iteration functions.
-// Every resource that returns a list of multiple results must implement this functionality, whether or not it is paged.
-// In addition to the methods provided here, each collection should also provide an AsItem(Page) method that
-// casts the Page to its more specific type and returns the Page's contents as a slice.
+// Collection must be satisfied by the result type of any resource collection.
+// It allows clients to interact with the resource uniformly, regardless of whether or not or how it's paginated.
type Collection interface {
- // Pager returns one of the concrete Pager implementations from this package, or a custom one.
- // The style of Pager returned determines how the collection is paged.
- Pager() Pager
+ // NextPageURL generates the URL for the page of data that follows this collection.
+ // Return "" if no such page exists.
+ NextPageURL() string
- // Concat the contents of another collection on to the end of this one.
- // Return a new collection that contains elements from both.
+ // Concat creates a new Collection that contains all of the elements from this page and another page.
+ // It's used to aggregate results for the AllPages method.
Concat(Collection) Collection
}
-// EachPage iterates through a Collection one page at a time.
-// The handler function will be invoked with a Collection containing each page.
-// If the handler returns true, iteration will continue. If it returns false, no more pages will be fetched.
-func EachPage(first Collection, handler func(Collection) bool) error {
- p := first.Pager()
- var err error
- current := first
+// Pager knows how to advance through a specific resource collection, one page at a time.
+type Pager struct {
+ initialURL string
+ advance func(string) (Collection, error)
+}
+
+// NewPager constructs a manually-configured pager.
+// Supply the URL for the first page and a function that requests a specific page given a URL.
+func NewPager(initialURL string, advance func(string) (Collection, error)) Pager {
+ return Pager{
+ initialURL: initialURL,
+ advance: advance,
+ }
+}
+
+// NewSinglePager constructs a Pager that "iterates" over a single-paged Collection.
+// Supply a function that returns the only page.
+func NewSinglePager(only func() (Collection, error)) Pager {
+ consumed := false
+ single := func(_ string) (Collection, error) {
+ if !consumed {
+ consumed = true
+ return only()
+ }
+ return nil, ErrPageNotAvailable
+ }
+
+ return Pager{
+ initialURL: "",
+ advance: single,
+ }
+}
+
+// EachPage iterates over each page returned by a Pager, yielding one at a time to a handler function.
+// Return "false" from the handler to prematurely stop iterating.
+func (p Pager) EachPage(handler func(Collection) bool) error {
+ currentURL := p.initialURL
for {
- if !handler(current) {
- return nil
- }
-
- if !p.HasNextPage() {
- return nil
- }
-
- current, err = p.NextPage()
+ currentPage, err := p.advance(currentURL)
if err != nil {
return err
}
+
+ if !handler(currentPage) {
+ return nil
+ }
+
+ currentURL = currentPage.NextPageURL()
+ if currentURL == "" {
+ return nil
+ }
}
}
-// AllPages consolidates all pages reachable from a provided starting point into a single mega-Page.
-// Use this only when you know that the full set will always fit within memory.
-func AllPages(first Collection) (Collection, error) {
- megaPage := first
- isFirst := true
+// AllPages accumulates every page reachable from a Pager into a single Collection, for convenience.
+func (p Pager) AllPages() (Collection, error) {
+ var megaPage Collection
- err := EachPage(first, func(page Collection) bool {
- if isFirst {
- isFirst = false
+ err := p.EachPage(func(page Collection) bool {
+ if megaPage == nil {
+ megaPage = page
} else {
megaPage = megaPage.Concat(page)
}
@@ -69,34 +91,6 @@
return megaPage, err
}
-// Pager describes a specific paging idiom for a Collection resource.
-// Generally, to use a Pager, the Collection must also implement a more specialized interface than Collection.
-// Clients should not generally interact with Pagers directly.
-// Instead, use the more convenient collection traversal methods: AllPages and EachPage.
-type Pager interface {
-
- // HasNextPage returns true if a call to NextPage will return an additional Page of results.
- HasNextPage() bool
-
- // NextPage returns the next Page in the sequence.
- // Panics if no page is available, so always check HasNextPage first.
- NextPage() (Collection, error)
-}
-
-// SinglePager is used by collections that are not actually paged.
-// It has no additional interface requirements for its host Page.
-type SinglePager struct{}
-
-// HasNextPage always reports false.
-func (p SinglePager) HasNextPage() bool {
- return false
-}
-
-// NextPage always returns an ErrPageNotAvailable.
-func (p SinglePager) NextPage() (Collection, error) {
- return nil, ErrPageNotAvailable
-}
-
// PaginationLinks stores the `next` and `previous` links that are provided by some (but not all) paginated resources.
type PaginationLinks struct {
@@ -106,59 +100,3 @@
// Previous is the full URL to the previous page of results, or nil if this is the first page.
Previous *string `json:"previous,omitempty"`
}
-
-// LinkCollection must be satisfied by a Page that uses a LinkPager.
-type LinkCollection interface {
- Collection
-
- // Service returns the client used to make further requests.
- Service() *ServiceClient
-
- // Links returns the pagination links from a single page.
- Links() PaginationLinks
-
- // Interpret an arbitrary JSON result as a new LinkCollection.
- Interpret(interface{}) (LinkCollection, error)
-}
-
-// LinkPager implements paging for collections that provide a link structure in their response JSON.
-// It follows explicit `next` links and stops when the `next` link is "null".
-type LinkPager struct {
- current LinkCollection
-}
-
-// NewLinkPager creates and initializes a pager for a LinkCollection.
-func NewLinkPager(first LinkCollection) *LinkPager {
- return &LinkPager{current: first}
-}
-
-// HasNextPage checks the `next` link in the pagination data.
-func (p *LinkPager) HasNextPage() bool {
- return p.current.Links().Next != nil
-}
-
-// NextPage follows the `next` link to construct the next page of data.
-func (p *LinkPager) NextPage() (Collection, error) {
- url := p.current.Links().Next
- if url == nil {
- return nil, ErrPageNotAvailable
- }
-
- var response interface{}
- _, err := perigee.Request("GET", *url, perigee.Options{
- MoreHeaders: p.current.Service().Provider.AuthenticatedHeaders(),
- Results: &response,
- OkCodes: []int{200},
- })
- if err != nil {
- return nil, err
- }
-
- interpreted, err := p.current.Interpret(response)
- if err != nil {
- return nil, err
- }
-
- p.current = interpreted
- return interpreted, nil
-}
diff --git a/collections_test.go b/collections_test.go
index 4b0fdc5..c83bd38 100644
--- a/collections_test.go
+++ b/collections_test.go
@@ -1,7 +1,6 @@
package gophercloud
import (
- "errors"
"fmt"
"net/http"
"reflect"
@@ -16,25 +15,27 @@
results []int
}
-func (c SinglePageCollection) Pager() Pager {
- return SinglePager{}
+func (c SinglePageCollection) NextPageURL() string {
+ panic("NextPageURL should never be called on a single-paged collection.")
}
func (c SinglePageCollection) Concat(other Collection) Collection {
panic("Concat should never be called on a single-paged collection.")
}
-func AsSingleInts(c Collection) []int {
+func ExtractSingleInts(c Collection) []int {
return c.(SinglePageCollection).results
}
-var single = SinglePageCollection{
- results: []int{1, 2, 3},
+func setupSinglePaged() Pager {
+ return NewSinglePager(func() (Collection, error) {
+ return SinglePageCollection{results: []int{1, 2, 3}}, nil
+ })
}
func TestEnumerateSinglePaged(t *testing.T) {
callCount := 0
- EachPage(single, func(page Collection) bool {
+ err := setupSinglePaged().EachPage(func(page Collection) bool {
callCount++
expected := []int{1, 2, 3}
@@ -44,6 +45,9 @@
}
return true
})
+ if err != nil {
+ t.Fatalf("Unexpected error calling EachPage: %v", err)
+ }
if callCount != 1 {
t.Errorf("Callback was invoked %d times", callCount)
@@ -51,13 +55,13 @@
}
func TestAllSinglePaged(t *testing.T) {
- r, err := AllPages(single)
+ r, err := setupSinglePaged().AllPages()
if err != nil {
t.Fatalf("Unexpected error when iterating pages: %v", err)
}
expected := []int{1, 2, 3}
- actual := AsSingleInts(r)
+ actual := ExtractSingleInts(r)
if !reflect.DeepEqual(expected, actual) {
t.Errorf("Expected %v, but was %v", expected, actual)
}
@@ -68,73 +72,36 @@
type LinkedCollection struct {
PaginationLinks
- service *ServiceClient
results []int
}
-func (c LinkedCollection) Pager() Pager {
- return NewLinkPager(c)
+func (page LinkedCollection) NextPageURL() string {
+ n := page.PaginationLinks.Next
+ if n == nil {
+ return ""
+ }
+ return *n
}
-func (c LinkedCollection) Concat(other Collection) Collection {
+func (page LinkedCollection) Concat(other Collection) Collection {
return LinkedCollection{
- service: c.service,
+ service: page.service,
results: append(c.results, AsLinkedInts(other)...),
}
}
-func (c LinkedCollection) Links() PaginationLinks {
- return c.PaginationLinks
-}
-
-func (c LinkedCollection) Service() *ServiceClient {
- return c.service
-}
-
-func (c LinkedCollection) Interpret(response interface{}) (LinkCollection, error) {
- casted, ok := response.([]interface{})
- if ok {
- asInts := make([]int, len(casted))
- for index, item := range casted {
- f := item.(float64)
- asInts[index] = int(f)
- }
-
- var nextURL *string
- switch asInts[0] {
- case 4:
- u := testhelper.Server.URL + "/foo?page=3&perPage=3"
- nextURL = &u
- case 7:
- // Leave nextURL as nil.
- default:
- return nil, fmt.Errorf("Unexpected resultset: %#v", asInts)
- }
-
- result := LinkedCollection{
- PaginationLinks: PaginationLinks{Next: nextURL},
- service: c.service,
- results: asInts,
- }
- return result, nil
- }
- return nil, errors.New("Wat")
-}
-
func AsLinkedInts(results Collection) []int {
return results.(LinkedCollection).results
}
-func createLinked() LinkedCollection {
+func createLinked() Pager {
nextURL := testhelper.Server.URL + "/foo?page=2&perPage=3"
- return LinkedCollection{
- PaginationLinks: PaginationLinks{Next: &nextURL},
- service: &ServiceClient{
- Provider: &ProviderClient{TokenID: "1234"},
- Endpoint: testhelper.Endpoint(),
- },
- results: []int{1, 2, 3},
- }
+ return CreatePager(func(url) Collection {
+ LinkedCollection{
+ PaginationLinks: PaginationLinks{Next: &nextURL},
+ results: []int{1, 2, 3},
+ }
+ })
}
func setupLinkedResponses(t *testing.T) {