Use base structs and embedding.
diff --git a/pagination.go b/pagination.go
index b0930e8..162db19 100644
--- a/pagination.go
+++ b/pagination.go
@@ -65,26 +65,61 @@
// Page 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.
+// Generally, rather than implementing this interface directly, implementors should embed one of the concrete PageBase structs,
+// instead.
type Page interface {
// NextPageURL generates the URL for the page of data that follows this collection.
// Return "" if no such page exists.
NextPageURL() (string, error)
+
+ // IsEmpty returns true if this Page has no items in it.
+ IsEmpty() (bool, error)
}
-// SinglePage is a page that contains all of the results from an operation.
-type SinglePage LastHTTPResponse
+// MarkerPage is a stricter Page interface that describes additional functionality required for use with NewMarkerPager.
+// For convenience, embed the MarkedPageBase struct.
+type MarkerPage interface {
+ Page
+
+ // LastMark returns the last "marker" value on this page.
+ LastMark() (string, error)
+}
+
+// nullPage is an always-empty page that trivially satisfies all Page interfacts.
+// It's useful to be returned along with an error.
+type nullPage struct{}
// NextPageURL always returns "" to indicate that there are no more pages to return.
-func (current SinglePage) NextPageURL() (string, error) {
+func (p nullPage) NextPageURL() (string, error) {
return "", nil
}
-// LinkedPage is a page in a collection that provides navigational "Next" and "Previous" links within its result.
-type LinkedPage LastHTTPResponse
+// IsEmpty always returns true to prevent iteration over nullPages.
+func (p nullPage) IsEmpty() (bool, error) {
+ return true, nil
+}
+
+// LastMark always returns "" because the nullPage contains no items to have a mark.
+func (p nullPage) LastMark() (string, error) {
+ return "", nil
+}
+
+// SinglePageBase may be embedded in a Page that contains all of the results from an operation at once.
+type SinglePageBase LastHTTPResponse
+
+// NextPageURL always returns "" to indicate that there are no more pages to return.
+func (current SinglePageBase) NextPageURL() (string, error) {
+ return "", nil
+}
+
+// LinkedPageBase may be embedded to implement a page that provides navigational "Next" and "Previous" links within its result.
+type LinkedPageBase LastHTTPResponse
// NextPageURL extracts the pagination structure from a JSON response and returns the "next" link, if one is present.
-func (current LinkedPage) NextPageURL() (string, error) {
+// It assumes that the links are available in a "links" element of the top-level response object.
+// If this is not the case, override NextPageURL on your result type.
+func (current LinkedPageBase) NextPageURL() (string, error) {
type response struct {
Links struct {
Next *string `mapstructure:"next,omitempty"`
@@ -104,19 +139,19 @@
return *r.Links.Next, nil
}
-// MarkerPage is a page in a collection that's paginated by "limit" and "marker" query parameters.
-type MarkerPage struct {
+// MarkerPageBase is a page in a collection that's paginated by "limit" and "marker" query parameters.
+type MarkerPageBase struct {
LastHTTPResponse
- // lastMark is a captured function that returns the final entry on a given page.
- lastMark func(Page) (string, error)
+ // A reference to the embedding struct.
+ Self MarkerPage
}
// NextPageURL generates the URL for the page of results after this one.
-func (current MarkerPage) NextPageURL() (string, error) {
- currentURL := current.LastHTTPResponse.URL
+func (current MarkerPageBase) NextPageURL() (string, error) {
+ currentURL := current.URL
- mark, err := current.lastMark(current)
+ mark, err := current.Self.LastMark()
if err != nil {
return "", err
}
@@ -133,50 +168,46 @@
initialURL string
fetchNextPage func(string) (Page, error)
-
- countPage func(Page) (int, error)
}
// NewPager constructs a manually-configured pager.
// Supply the URL for the first page, a function that requests a specific page given a URL, and a function that counts a page.
-func NewPager(initialURL string, fetchNextPage func(string) (Page, error), countPage func(Page) (int, error)) Pager {
+func NewPager(initialURL string, fetchNextPage func(string) (Page, error)) Pager {
return Pager{
initialURL: initialURL,
fetchNextPage: fetchNextPage,
- countPage: countPage,
}
}
// NewSinglePager constructs a Pager that "iterates" over a single Page.
-// Supply the URL to request.
-func NewSinglePager(client *ServiceClient, onlyURL string, countPage func(Page) (int, error)) Pager {
+// Supply the URL to request and a function that creates a Page of the appropriate type.
+func NewSinglePager(client *ServiceClient, onlyURL string, createPage func(resp LastHTTPResponse) Page) Pager {
consumed := false
single := func(_ string) (Page, error) {
if !consumed {
consumed = true
resp, err := request(client, onlyURL)
if err != nil {
- return SinglePage{}, err
+ return nullPage{}, err
}
cp, err := RememberHTTPResponse(resp)
if err != nil {
- return SinglePage{}, err
+ return nullPage{}, err
}
- return SinglePage(cp), nil
+ return createPage(cp), nil
}
- return SinglePage{}, ErrPageNotAvailable
+ return nullPage{}, ErrPageNotAvailable
}
return Pager{
initialURL: "",
fetchNextPage: single,
- countPage: countPage,
}
}
// NewLinkedPager creates a Pager that uses a "links" element in the JSON response to locate the next page.
-func NewLinkedPager(client *ServiceClient, initialURL string, countPage func(Page) (int, error)) Pager {
+func NewLinkedPager(client *ServiceClient, initialURL string, createPage func(resp LastHTTPResponse) Page) Pager {
fetchNextPage := func(url string) (Page, error) {
resp, err := request(client, url)
if err != nil {
@@ -188,39 +219,36 @@
return nil, err
}
- return LinkedPage(cp), nil
+ return createPage(cp), nil
}
return Pager{
initialURL: initialURL,
fetchNextPage: fetchNextPage,
- countPage: countPage,
}
}
// NewMarkerPager creates a Pager that iterates over successive pages by issuing requests with a "marker" parameter set to the
// final element of the previous Page.
-func NewMarkerPager(client *ServiceClient, initialURL string,
- lastMark func(Page) (string, error), countPage func(Page) (int, error)) Pager {
+func NewMarkerPager(client *ServiceClient, initialURL string, createPage func(resp LastHTTPResponse) MarkerPage) Pager {
fetchNextPage := func(currentURL string) (Page, error) {
resp, err := request(client, currentURL)
if err != nil {
- return nil, err
+ return nullPage{}, err
}
last, err := RememberHTTPResponse(resp)
if err != nil {
- return nil, err
+ return nullPage{}, err
}
- return MarkerPage{LastHTTPResponse: last, lastMark: lastMark}, nil
+ return createPage(last), nil
}
return Pager{
initialURL: initialURL,
fetchNextPage: fetchNextPage,
- countPage: countPage,
}
}
@@ -234,11 +262,11 @@
return err
}
- count, err := p.countPage(currentPage)
+ empty, err := currentPage.IsEmpty()
if err != nil {
return err
}
- if count == 0 {
+ if empty {
return nil
}
diff --git a/pagination_test.go b/pagination_test.go
index eb773bd..8ce36a2 100644
--- a/pagination_test.go
+++ b/pagination_test.go
@@ -20,12 +20,24 @@
// SinglePage sample and test cases.
+type SinglePageResult struct {
+ SinglePageBase
+}
+
+func (r SinglePageResult) IsEmpty() (bool, error) {
+ is, err := ExtractSingleInts(r)
+ if err != nil {
+ return true, err
+ }
+ return len(is) == 0, nil
+}
+
func ExtractSingleInts(page Page) ([]int, error) {
var response struct {
Ints []int `mapstructure:"ints"`
}
- err := mapstructure.Decode(page.(SinglePage).Body, &response)
+ err := mapstructure.Decode(page.(SinglePageResult).Body, &response)
if err != nil {
return nil, err
}
@@ -42,15 +54,11 @@
fmt.Fprintf(w, `{ "ints": [1, 2, 3] }`)
})
- countPage := func(p Page) (int, error) {
- is, err := ExtractSingleInts(p)
- if err != nil {
- return 0, err
- }
- return len(is), nil
+ createPage := func(r LastHTTPResponse) Page {
+ return SinglePageResult{SinglePageBase(r)}
}
- return NewSinglePager(client, testhelper.Server.URL+"/only", countPage)
+ return NewSinglePager(client, testhelper.Server.URL+"/only", createPage)
}
func TestEnumerateSinglePaged(t *testing.T) {
@@ -63,31 +71,34 @@
expected := []int{1, 2, 3}
actual, err := ExtractSingleInts(page)
- if err != nil {
- return false, err
- }
- if !reflect.DeepEqual(expected, actual) {
- t.Errorf("Expected %v, but was %v", expected, actual)
- }
+ testhelper.AssertNoErr(t, err)
+ testhelper.CheckDeepEquals(t, expected, actual)
return true, nil
})
- if err != nil {
- t.Fatalf("Unexpected error calling EachPage: %v", err)
- }
-
- if callCount != 1 {
- t.Errorf("Callback was invoked %d times", callCount)
- }
+ testhelper.CheckNoErr(t, err)
+ testhelper.CheckEquals(t, 1, callCount)
}
// LinkedPager sample and test cases.
+type LinkedPageResult struct {
+ LinkedPageBase
+}
+
+func (r LinkedPageResult) IsEmpty() (bool, error) {
+ is, err := ExtractLinkedInts(r)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
func ExtractLinkedInts(page Page) ([]int, error) {
var response struct {
Ints []int `mapstructure:"ints"`
}
- err := mapstructure.Decode(page.(LinkedPage).Body, &response)
+ err := mapstructure.Decode(page.(LinkedPageResult).Body, &response)
if err != nil {
return nil, err
}
@@ -115,15 +126,11 @@
client := createClient()
- countPage := func(p Page) (int, error) {
- is, err := ExtractLinkedInts(p)
- if err != nil {
- return 0, err
- }
- return len(is), nil
+ createPage := func(r LastHTTPResponse) Page {
+ return LinkedPageResult{LinkedPageBase(r)}
}
- return NewLinkedPager(client, testhelper.Server.URL+"/page1", countPage)
+ return NewLinkedPager(client, testhelper.Server.URL+"/page1", createPage)
}
func TestEnumerateLinked(t *testing.T) {
@@ -168,6 +175,31 @@
}
}
+// MarkerPager sample and test cases.
+
+type MarkerPageResult struct {
+ MarkerPageBase
+}
+
+func (r MarkerPageResult) IsEmpty() (bool, error) {
+ results, err := ExtractMarkerStrings(r)
+ if err != nil {
+ return true, err
+ }
+ return len(results) == 0, err
+}
+
+func (r MarkerPageResult) LastMark() (string, error) {
+ results, err := ExtractMarkerStrings(r)
+ if err != nil {
+ return "", err
+ }
+ if len(results) == 0 {
+ return "", nil
+ }
+ return results[len(results)-1], nil
+}
+
func createMarkerPaged(t *testing.T) Pager {
testhelper.SetupHTTP()
@@ -190,28 +222,17 @@
client := createClient()
- lastMark := func(p Page) (string, error) {
- items, err := ExtractMarkerStrings(p)
- if err != nil {
- return "", err
- }
- return items[len(items)-1], nil
+ createPage := func(r LastHTTPResponse) MarkerPage {
+ p := MarkerPageResult{MarkerPageBase{LastHTTPResponse: r}}
+ p.MarkerPageBase.Self = p
+ return p
}
- countPage := func(p Page) (int, error) {
- items, err := ExtractMarkerStrings(p)
- if err != nil {
- return 0, err
- }
- fmt.Printf("Counting items [%#v] = [%d]\n", items, len(items))
- return len(items), nil
- }
-
- return NewMarkerPager(client, testhelper.Server.URL+"/page", lastMark, countPage)
+ return NewMarkerPager(client, testhelper.Server.URL+"/page", createPage)
}
func ExtractMarkerStrings(page Page) ([]string, error) {
- content := page.(MarkerPage).Body.([]uint8)
+ content := page.(MarkerPageResult).Body.([]uint8)
parts := strings.Split(string(content), "\n")
results := make([]string, 0, len(parts))
for _, part := range parts {