blob: 4b646a1140285d3c842c7719f3289829a29da74d [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.
112 lastMark func(MarkerPage) (string, error)
113}
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 Wilsone30b76b2014-09-12 08:36:17 -0400136}
137
138// NewPager constructs a manually-configured pager.
139// Supply the URL for the first page and a function that requests a specific page given a URL.
Ash Wilson3658d382014-09-15 08:12:33 -0400140func NewPager(initialURL string, fetchNextPage func(string) (Page, error)) Pager {
Ash Wilsone30b76b2014-09-12 08:36:17 -0400141 return Pager{
Ash Wilson3658d382014-09-15 08:12:33 -0400142 initialURL: initialURL,
143 fetchNextPage: fetchNextPage,
Ash Wilsone30b76b2014-09-12 08:36:17 -0400144 }
145}
146
Ash Wilson36986c62014-09-15 09:56:34 -0400147// NewSinglePager constructs a Pager that "iterates" over a single Page.
148// Supply the URL to request.
149func NewSinglePager(client *ServiceClient, onlyURL string) Pager {
150 consumed := false
151 single := func(_ string) (Page, error) {
152 if !consumed {
153 consumed = true
154 resp, err := request(client, onlyURL)
155 if err != nil {
156 return SinglePage{}, err
157 }
158
159 cp, err := RememberHTTPResponse(resp)
160 if err != nil {
161 return SinglePage{}, err
162 }
163 return SinglePage(cp), nil
164 }
165 return SinglePage{}, ErrPageNotAvailable
166 }
167
168 return Pager{
169 initialURL: "",
170 fetchNextPage: single,
171 }
172}
173
174// NewLinkedPager creates a Pager that uses a "links" element in the JSON response to locate the next page.
175func NewLinkedPager(client *ServiceClient, initialURL string) Pager {
176 fetchNextPage := func(url string) (Page, error) {
177 resp, err := request(client, url)
178 if err != nil {
179 return nil, err
180 }
181
182 cp, err := RememberHTTPResponse(resp)
183 if err != nil {
184 return nil, err
185 }
186
187 return LinkedPage(cp), nil
188 }
189
190 return Pager{
191 initialURL: initialURL,
192 fetchNextPage: fetchNextPage,
193 }
194}
195
Ash Wilson993cf322014-09-15 14:34:12 -0400196// NewMarkerPager creates a Pager that iterates over successive pages by issuing requests with a "marker" parameter set to the
197// final element of the previous Page.
198func NewMarkerPager(client *ServiceClient, initialURL string, lastMark func(MarkerPage) (string, error)) Pager {
199 fetchNextPage := func(currentURL string) (Page, error) {
200 resp, err := request(client, currentURL)
201 if err != nil {
202 return nil, err
203 }
204
205 last, err := RememberHTTPResponse(resp)
206 if err != nil {
207 return nil, err
208 }
209
210 return MarkerPage{LastHTTPResponse: last, lastMark: lastMark}, nil
211 }
212
213 return Pager{
214 initialURL: initialURL,
215 fetchNextPage: fetchNextPage,
216 }
217}
218
Ash Wilsone30b76b2014-09-12 08:36:17 -0400219// EachPage iterates over each page returned by a Pager, yielding one at a time to a handler function.
220// Return "false" from the handler to prematurely stop iterating.
Ash Wilson6b35e502014-09-12 15:15:23 -0400221func (p Pager) EachPage(handler func(Page) (bool, error)) error {
Ash Wilsone30b76b2014-09-12 08:36:17 -0400222 currentURL := p.initialURL
Ash Wilson64d67b22014-09-05 13:04:12 -0400223 for {
Ash Wilson3658d382014-09-15 08:12:33 -0400224 currentPage, err := p.fetchNextPage(currentURL)
Ash Wilson64d67b22014-09-05 13:04:12 -0400225 if err != nil {
226 return err
227 }
Ash Wilsone30b76b2014-09-12 08:36:17 -0400228
Ash Wilson6b35e502014-09-12 15:15:23 -0400229 ok, err := handler(currentPage)
230 if err != nil {
231 return err
232 }
233 if !ok {
Ash Wilsone30b76b2014-09-12 08:36:17 -0400234 return nil
235 }
236
Ash Wilson976d2e62014-09-12 13:29:29 -0400237 currentURL, err = currentPage.NextPageURL()
238 if err != nil {
239 return err
240 }
Ash Wilsone30b76b2014-09-12 08:36:17 -0400241 if currentURL == "" {
242 return nil
243 }
Ash Wilson64d67b22014-09-05 13:04:12 -0400244 }
245}