blob: b0930e88d916f7a37e52bc29fb45844596348377 [file] [log] [blame]
Ash Wilson64d67b22014-09-05 13:04:12 -04001package gophercloud
2
Ash Wilson5bf6f662014-09-12 12:31:17 -04003import (
4 "encoding/json"
5 "errors"
6 "io/ioutil"
7 "net/http"
Ash Wilson993cf322014-09-15 14:34:12 -04008 "net/url"
Ash Wilson5bf6f662014-09-12 12:31:17 -04009
10 "github.com/mitchellh/mapstructure"
Ash Wilson18f32522014-09-15 08:52:12 -040011 "github.com/racker/perigee"
Ash Wilson5bf6f662014-09-12 12:31:17 -040012)
Ash Wilson64d67b22014-09-05 13:04:12 -040013
14var (
15 // ErrPageNotAvailable is returned from a Pager when a next or previous page is requested, but does not exist.
16 ErrPageNotAvailable = errors.New("The requested Collection page does not exist.")
17)
18
Ash Wilson36986c62014-09-15 09:56:34 -040019// LastHTTPResponse stores generic information derived from an HTTP response.
20type LastHTTPResponse struct {
Ash Wilson993cf322014-09-15 14:34:12 -040021 url.URL
Ash Wilson36986c62014-09-15 09:56:34 -040022 http.Header
Ash Wilsonf52b08b2014-09-15 10:00:43 -040023 Body interface{}
Ash Wilson36986c62014-09-15 09:56:34 -040024}
25
26// RememberHTTPResponse parses an HTTP response as JSON and returns a LastHTTPResponse containing the results.
27// The main reason to do this instead of holding the response directly is that a response body can only be read once.
28// Also, this centralizes the JSON decoding.
29func RememberHTTPResponse(resp http.Response) (LastHTTPResponse, error) {
Ash Wilsonf52b08b2014-09-15 10:00:43 -040030 var parsedBody interface{}
Ash Wilson36986c62014-09-15 09:56:34 -040031
32 defer resp.Body.Close()
33 rawBody, err := ioutil.ReadAll(resp.Body)
34 if err != nil {
35 return LastHTTPResponse{}, err
36 }
Ash Wilson993cf322014-09-15 14:34:12 -040037
38 if resp.Header.Get("Content-Type") == "application/json" {
39 err = json.Unmarshal(rawBody, &parsedBody)
40 if err != nil {
41 return LastHTTPResponse{}, err
42 }
43 } else {
44 parsedBody = rawBody
Ash Wilson36986c62014-09-15 09:56:34 -040045 }
46
Ash Wilson993cf322014-09-15 14:34:12 -040047 return LastHTTPResponse{
48 URL: *resp.Request.URL,
49 Header: resp.Header,
50 Body: parsedBody,
51 }, err
Ash Wilson36986c62014-09-15 09:56:34 -040052}
53
54// request performs a Perigee request and extracts the http.Response from the result.
55func request(client *ServiceClient, url string) (http.Response, error) {
56 resp, err := perigee.Request("GET", url, perigee.Options{
57 MoreHeaders: client.Provider.AuthenticatedHeaders(),
Ash Wilson993cf322014-09-15 14:34:12 -040058 OkCodes: []int{200, 204},
Ash Wilson36986c62014-09-15 09:56:34 -040059 })
60 if err != nil {
61 return http.Response{}, err
62 }
63 return resp.HttpResponse, nil
64}
65
Ash Wilson5bf6f662014-09-12 12:31:17 -040066// Page must be satisfied by the result type of any resource collection.
Ash Wilsone30b76b2014-09-12 08:36:17 -040067// It allows clients to interact with the resource uniformly, regardless of whether or not or how it's paginated.
Ash Wilson5bf6f662014-09-12 12:31:17 -040068type Page interface {
Ash Wilson64d67b22014-09-05 13:04:12 -040069
Ash Wilsone30b76b2014-09-12 08:36:17 -040070 // NextPageURL generates the URL for the page of data that follows this collection.
71 // Return "" if no such page exists.
Ash Wilson976d2e62014-09-12 13:29:29 -040072 NextPageURL() (string, error)
Ash Wilson64d67b22014-09-05 13:04:12 -040073}
74
Ash Wilson36986c62014-09-15 09:56:34 -040075// SinglePage is a page that contains all of the results from an operation.
76type SinglePage LastHTTPResponse
77
78// NextPageURL always returns "" to indicate that there are no more pages to return.
79func (current SinglePage) NextPageURL() (string, error) {
80 return "", nil
81}
82
83// LinkedPage is a page in a collection that provides navigational "Next" and "Previous" links within its result.
84type LinkedPage LastHTTPResponse
85
86// NextPageURL extracts the pagination structure from a JSON response and returns the "next" link, if one is present.
87func (current LinkedPage) NextPageURL() (string, error) {
88 type response struct {
89 Links struct {
90 Next *string `mapstructure:"next,omitempty"`
91 } `mapstructure:"links"`
92 }
93
94 var r response
95 err := mapstructure.Decode(current.Body, &r)
96 if err != nil {
97 return "", err
98 }
99
100 if r.Links.Next == nil {
101 return "", nil
102 }
103
104 return *r.Links.Next, nil
105}
106
Ash Wilson993cf322014-09-15 14:34:12 -0400107// MarkerPage is a page in a collection that's paginated by "limit" and "marker" query parameters.
108type MarkerPage struct {
109 LastHTTPResponse
110
111 // lastMark is a captured function that returns the final entry on a given page.
Ash Wilson5a25f542014-09-15 15:43:20 -0400112 lastMark func(Page) (string, error)
Ash Wilson993cf322014-09-15 14:34:12 -0400113}
114
115// NextPageURL generates the URL for the page of results after this one.
116func (current MarkerPage) NextPageURL() (string, error) {
117 currentURL := current.LastHTTPResponse.URL
118
119 mark, err := current.lastMark(current)
120 if err != nil {
121 return "", err
122 }
123
124 q := currentURL.Query()
125 q.Set("marker", mark)
126 currentURL.RawQuery = q.Encode()
127
128 return currentURL.String(), nil
129}
130
Ash Wilsone30b76b2014-09-12 08:36:17 -0400131// Pager knows how to advance through a specific resource collection, one page at a time.
132type Pager struct {
133 initialURL string
Ash Wilson64d67b22014-09-05 13:04:12 -0400134
Ash Wilson3658d382014-09-15 08:12:33 -0400135 fetchNextPage func(string) (Page, error)
Ash Wilson5a25f542014-09-15 15:43:20 -0400136
137 countPage func(Page) (int, error)
Ash Wilsone30b76b2014-09-12 08:36:17 -0400138}
139
140// NewPager constructs a manually-configured pager.
Ash Wilson5a25f542014-09-15 15:43:20 -0400141// Supply the URL for the first page, a function that requests a specific page given a URL, and a function that counts a page.
142func NewPager(initialURL string, fetchNextPage func(string) (Page, error), countPage func(Page) (int, error)) Pager {
Ash Wilsone30b76b2014-09-12 08:36:17 -0400143 return Pager{
Ash Wilson3658d382014-09-15 08:12:33 -0400144 initialURL: initialURL,
145 fetchNextPage: fetchNextPage,
Ash Wilson5a25f542014-09-15 15:43:20 -0400146 countPage: countPage,
Ash Wilsone30b76b2014-09-12 08:36:17 -0400147 }
148}
149
Ash Wilson36986c62014-09-15 09:56:34 -0400150// NewSinglePager constructs a Pager that "iterates" over a single Page.
151// Supply the URL to request.
Ash Wilson5a25f542014-09-15 15:43:20 -0400152func NewSinglePager(client *ServiceClient, onlyURL string, countPage func(Page) (int, error)) Pager {
Ash Wilson36986c62014-09-15 09:56:34 -0400153 consumed := false
154 single := func(_ string) (Page, error) {
155 if !consumed {
156 consumed = true
157 resp, err := request(client, onlyURL)
158 if err != nil {
159 return SinglePage{}, err
160 }
161
162 cp, err := RememberHTTPResponse(resp)
163 if err != nil {
164 return SinglePage{}, err
165 }
166 return SinglePage(cp), nil
167 }
168 return SinglePage{}, ErrPageNotAvailable
169 }
170
171 return Pager{
172 initialURL: "",
173 fetchNextPage: single,
Ash Wilson5a25f542014-09-15 15:43:20 -0400174 countPage: countPage,
Ash Wilson36986c62014-09-15 09:56:34 -0400175 }
176}
177
178// NewLinkedPager creates a Pager that uses a "links" element in the JSON response to locate the next page.
Ash Wilson5a25f542014-09-15 15:43:20 -0400179func NewLinkedPager(client *ServiceClient, initialURL string, countPage func(Page) (int, error)) Pager {
Ash Wilson36986c62014-09-15 09:56:34 -0400180 fetchNextPage := func(url string) (Page, error) {
181 resp, err := request(client, url)
182 if err != nil {
183 return nil, err
184 }
185
186 cp, err := RememberHTTPResponse(resp)
187 if err != nil {
188 return nil, err
189 }
190
191 return LinkedPage(cp), nil
192 }
193
194 return Pager{
195 initialURL: initialURL,
196 fetchNextPage: fetchNextPage,
Ash Wilson5a25f542014-09-15 15:43:20 -0400197 countPage: countPage,
Ash Wilson36986c62014-09-15 09:56:34 -0400198 }
199}
200
Ash Wilson993cf322014-09-15 14:34:12 -0400201// NewMarkerPager creates a Pager that iterates over successive pages by issuing requests with a "marker" parameter set to the
202// final element of the previous Page.
Ash Wilson5a25f542014-09-15 15:43:20 -0400203func NewMarkerPager(client *ServiceClient, initialURL string,
204 lastMark func(Page) (string, error), countPage func(Page) (int, error)) Pager {
205
Ash Wilson993cf322014-09-15 14:34:12 -0400206 fetchNextPage := func(currentURL string) (Page, error) {
207 resp, err := request(client, currentURL)
208 if err != nil {
209 return nil, err
210 }
211
212 last, err := RememberHTTPResponse(resp)
213 if err != nil {
214 return nil, err
215 }
216
217 return MarkerPage{LastHTTPResponse: last, lastMark: lastMark}, nil
218 }
219
220 return Pager{
221 initialURL: initialURL,
222 fetchNextPage: fetchNextPage,
Ash Wilson5a25f542014-09-15 15:43:20 -0400223 countPage: countPage,
Ash Wilson993cf322014-09-15 14:34:12 -0400224 }
225}
226
Ash Wilsone30b76b2014-09-12 08:36:17 -0400227// EachPage iterates over each page returned by a Pager, yielding one at a time to a handler function.
228// Return "false" from the handler to prematurely stop iterating.
Ash Wilson6b35e502014-09-12 15:15:23 -0400229func (p Pager) EachPage(handler func(Page) (bool, error)) error {
Ash Wilsone30b76b2014-09-12 08:36:17 -0400230 currentURL := p.initialURL
Ash Wilson64d67b22014-09-05 13:04:12 -0400231 for {
Ash Wilson3658d382014-09-15 08:12:33 -0400232 currentPage, err := p.fetchNextPage(currentURL)
Ash Wilson64d67b22014-09-05 13:04:12 -0400233 if err != nil {
234 return err
235 }
Ash Wilsone30b76b2014-09-12 08:36:17 -0400236
Ash Wilson5a25f542014-09-15 15:43:20 -0400237 count, err := p.countPage(currentPage)
238 if err != nil {
239 return err
240 }
241 if count == 0 {
242 return nil
243 }
244
Ash Wilson6b35e502014-09-12 15:15:23 -0400245 ok, err := handler(currentPage)
246 if err != nil {
247 return err
248 }
249 if !ok {
Ash Wilsone30b76b2014-09-12 08:36:17 -0400250 return nil
251 }
252
Ash Wilson976d2e62014-09-12 13:29:29 -0400253 currentURL, err = currentPage.NextPageURL()
254 if err != nil {
255 return err
256 }
Ash Wilsone30b76b2014-09-12 08:36:17 -0400257 if currentURL == "" {
258 return nil
259 }
Ash Wilson64d67b22014-09-05 13:04:12 -0400260 }
261}