Initial implementation of MarkerPage.
diff --git a/pagination.go b/pagination.go
index 004c542..4b646a1 100644
--- a/pagination.go
+++ b/pagination.go
@@ -5,6 +5,7 @@
"errors"
"io/ioutil"
"net/http"
+ "net/url"
"github.com/mitchellh/mapstructure"
"github.com/racker/perigee"
@@ -17,6 +18,7 @@
// LastHTTPResponse stores generic information derived from an HTTP response.
type LastHTTPResponse struct {
+ url.URL
http.Header
Body interface{}
}
@@ -32,19 +34,28 @@
if err != nil {
return LastHTTPResponse{}, err
}
- err = json.Unmarshal(rawBody, &parsedBody)
- if err != nil {
- return LastHTTPResponse{}, err
+
+ if resp.Header.Get("Content-Type") == "application/json" {
+ err = json.Unmarshal(rawBody, &parsedBody)
+ if err != nil {
+ return LastHTTPResponse{}, err
+ }
+ } else {
+ parsedBody = rawBody
}
- return LastHTTPResponse{Header: resp.Header, Body: parsedBody}, err
+ return LastHTTPResponse{
+ URL: *resp.Request.URL,
+ Header: resp.Header,
+ Body: parsedBody,
+ }, err
}
// request performs a Perigee request and extracts the http.Response from the result.
func request(client *ServiceClient, url string) (http.Response, error) {
resp, err := perigee.Request("GET", url, perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
- OkCodes: []int{200},
+ OkCodes: []int{200, 204},
})
if err != nil {
return http.Response{}, err
@@ -93,6 +104,30 @@
return *r.Links.Next, nil
}
+// MarkerPage is a page in a collection that's paginated by "limit" and "marker" query parameters.
+type MarkerPage struct {
+ LastHTTPResponse
+
+ // lastMark is a captured function that returns the final entry on a given page.
+ lastMark func(MarkerPage) (string, error)
+}
+
+// NextPageURL generates the URL for the page of results after this one.
+func (current MarkerPage) NextPageURL() (string, error) {
+ currentURL := current.LastHTTPResponse.URL
+
+ mark, err := current.lastMark(current)
+ if err != nil {
+ return "", err
+ }
+
+ q := currentURL.Query()
+ q.Set("marker", mark)
+ currentURL.RawQuery = q.Encode()
+
+ return currentURL.String(), nil
+}
+
// Pager knows how to advance through a specific resource collection, one page at a time.
type Pager struct {
initialURL string
@@ -158,6 +193,29 @@
}
}
+// 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(MarkerPage) (string, error)) Pager {
+ fetchNextPage := func(currentURL string) (Page, error) {
+ resp, err := request(client, currentURL)
+ if err != nil {
+ return nil, err
+ }
+
+ last, err := RememberHTTPResponse(resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return MarkerPage{LastHTTPResponse: last, lastMark: lastMark}, nil
+ }
+
+ return Pager{
+ initialURL: initialURL,
+ fetchNextPage: fetchNextPage,
+ }
+}
+
// 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(Page) (bool, error)) error {
diff --git a/pagination_test.go b/pagination_test.go
index d215c33..a1c59bb 100644
--- a/pagination_test.go
+++ b/pagination_test.go
@@ -4,6 +4,7 @@
"fmt"
"net/http"
"reflect"
+ "strings"
"testing"
"github.com/mitchellh/mapstructure"
@@ -37,6 +38,7 @@
client := createClient()
testhelper.Mux.HandleFunc("/only", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, `{ "ints": [1, 2, 3] }`)
})
@@ -89,14 +91,17 @@
testhelper.SetupHTTP()
testhelper.Mux.HandleFunc("/page1", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, `{ "ints": [1, 2, 3], "links": { "next": "%s/page2" } }`, testhelper.Server.URL)
})
testhelper.Mux.HandleFunc("/page2", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, `{ "ints": [4, 5, 6], "links": { "next": "%s/page3" } }`, testhelper.Server.URL)
})
testhelper.Mux.HandleFunc("/page3", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, `{ "ints": [7, 8, 9], "links": { "next": null } }`)
})
@@ -146,3 +151,74 @@
t.Errorf("Expected 3 calls, but was %d", callCount)
}
}
+
+func createMarkerPaged(t *testing.T) Pager {
+ testhelper.SetupHTTP()
+
+ testhelper.Mux.HandleFunc("/page", func(w http.ResponseWriter, r *http.Request) {
+ r.ParseForm()
+ ms := r.Form["marker"]
+ switch {
+ case len(ms) == 0:
+ fmt.Fprintf(w, "aaa\nbbb\nccc")
+ case len(ms) == 1 && ms[0] == "ccc":
+ fmt.Fprintf(w, "ddd\neee\nfff")
+ case len(ms) == 1 && ms[0] == "fff":
+ fmt.Fprintf(w, "ggg\nhhh\niii")
+ case len(ms) == 1 && ms[0] == "iii":
+ w.WriteHeader(http.StatusNoContent)
+ default:
+ t.Errorf("Request with unexpected marker: [%v]", ms)
+ }
+ })
+
+ client := createClient()
+
+ return NewMarkerPager(client, testhelper.Server.URL+"/page", func(p MarkerPage) (string, error) {
+ items, err := ExtractMarkerStrings(p)
+ if err != nil {
+ return "", err
+ }
+ return items[len(items)-1], nil
+ })
+}
+
+func ExtractMarkerStrings(page Page) ([]string, error) {
+ content := page.(MarkerPage).Body.([]uint8)
+ return strings.Split(string(content), "\n"), nil
+}
+
+func TestEnumerateMarker(t *testing.T) {
+ pager := createMarkerPaged(t)
+ defer testhelper.TeardownHTTP()
+
+ callCount := 0
+ err := pager.EachPage(func(page Page) (bool, error) {
+ actual, err := ExtractMarkerStrings(page)
+ if err != nil {
+ return false, err
+ }
+
+ t.Logf("Handler invoked with %v", actual)
+
+ var expected []string
+ switch callCount {
+ case 0:
+ expected = []string{"aaa", "bbb", "ccc"}
+ case 1:
+ expected = []string{"ddd", "eee", "fff"}
+ case 2:
+ expected = []string{"ggg", "hhh", "iii"}
+ default:
+ t.Fatalf("Unexpected call count: %d", callCount)
+ return false, nil
+ }
+
+ testhelper.CheckDeepEquals(t, expected, actual)
+
+ callCount++
+ return true, nil
+ })
+ testhelper.AssertNoErr(t, err)
+ testhelper.AssertEquals(t, callCount, 3)
+}