blob: 004c5422f1ac48147a09f14d4f6938104caf8aa7 [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"
8
9 "github.com/mitchellh/mapstructure"
Ash Wilson18f32522014-09-15 08:52:12 -040010 "github.com/racker/perigee"
Ash Wilson5bf6f662014-09-12 12:31:17 -040011)
Ash Wilson64d67b22014-09-05 13:04:12 -040012
13var (
14 // ErrPageNotAvailable is returned from a Pager when a next or previous page is requested, but does not exist.
15 ErrPageNotAvailable = errors.New("The requested Collection page does not exist.")
16)
17
Ash Wilson36986c62014-09-15 09:56:34 -040018// LastHTTPResponse stores generic information derived from an HTTP response.
19type LastHTTPResponse struct {
20 http.Header
Ash Wilsonf52b08b2014-09-15 10:00:43 -040021 Body interface{}
Ash Wilson36986c62014-09-15 09:56:34 -040022}
23
24// RememberHTTPResponse parses an HTTP response as JSON and returns a LastHTTPResponse containing the results.
25// The main reason to do this instead of holding the response directly is that a response body can only be read once.
26// Also, this centralizes the JSON decoding.
27func RememberHTTPResponse(resp http.Response) (LastHTTPResponse, error) {
Ash Wilsonf52b08b2014-09-15 10:00:43 -040028 var parsedBody interface{}
Ash Wilson36986c62014-09-15 09:56:34 -040029
30 defer resp.Body.Close()
31 rawBody, err := ioutil.ReadAll(resp.Body)
32 if err != nil {
33 return LastHTTPResponse{}, err
34 }
35 err = json.Unmarshal(rawBody, &parsedBody)
36 if err != nil {
37 return LastHTTPResponse{}, err
38 }
39
40 return LastHTTPResponse{Header: resp.Header, Body: parsedBody}, err
41}
42
43// request performs a Perigee request and extracts the http.Response from the result.
44func request(client *ServiceClient, url string) (http.Response, error) {
45 resp, err := perigee.Request("GET", url, perigee.Options{
46 MoreHeaders: client.Provider.AuthenticatedHeaders(),
47 OkCodes: []int{200},
48 })
49 if err != nil {
50 return http.Response{}, err
51 }
52 return resp.HttpResponse, nil
53}
54
Ash Wilson5bf6f662014-09-12 12:31:17 -040055// Page must be satisfied by the result type of any resource collection.
Ash Wilsone30b76b2014-09-12 08:36:17 -040056// 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 -040057type Page interface {
Ash Wilson64d67b22014-09-05 13:04:12 -040058
Ash Wilsone30b76b2014-09-12 08:36:17 -040059 // NextPageURL generates the URL for the page of data that follows this collection.
60 // Return "" if no such page exists.
Ash Wilson976d2e62014-09-12 13:29:29 -040061 NextPageURL() (string, error)
Ash Wilson64d67b22014-09-05 13:04:12 -040062}
63
Ash Wilson36986c62014-09-15 09:56:34 -040064// SinglePage is a page that contains all of the results from an operation.
65type SinglePage LastHTTPResponse
66
67// NextPageURL always returns "" to indicate that there are no more pages to return.
68func (current SinglePage) NextPageURL() (string, error) {
69 return "", nil
70}
71
72// LinkedPage is a page in a collection that provides navigational "Next" and "Previous" links within its result.
73type LinkedPage LastHTTPResponse
74
75// NextPageURL extracts the pagination structure from a JSON response and returns the "next" link, if one is present.
76func (current LinkedPage) NextPageURL() (string, error) {
77 type response struct {
78 Links struct {
79 Next *string `mapstructure:"next,omitempty"`
80 } `mapstructure:"links"`
81 }
82
83 var r response
84 err := mapstructure.Decode(current.Body, &r)
85 if err != nil {
86 return "", err
87 }
88
89 if r.Links.Next == nil {
90 return "", nil
91 }
92
93 return *r.Links.Next, nil
94}
95
Ash Wilsone30b76b2014-09-12 08:36:17 -040096// Pager knows how to advance through a specific resource collection, one page at a time.
97type Pager struct {
98 initialURL string
Ash Wilson64d67b22014-09-05 13:04:12 -040099
Ash Wilson3658d382014-09-15 08:12:33 -0400100 fetchNextPage func(string) (Page, error)
Ash Wilsone30b76b2014-09-12 08:36:17 -0400101}
102
103// NewPager constructs a manually-configured pager.
104// 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 -0400105func NewPager(initialURL string, fetchNextPage func(string) (Page, error)) Pager {
Ash Wilsone30b76b2014-09-12 08:36:17 -0400106 return Pager{
Ash Wilson3658d382014-09-15 08:12:33 -0400107 initialURL: initialURL,
108 fetchNextPage: fetchNextPage,
Ash Wilsone30b76b2014-09-12 08:36:17 -0400109 }
110}
111
Ash Wilson36986c62014-09-15 09:56:34 -0400112// NewSinglePager constructs a Pager that "iterates" over a single Page.
113// Supply the URL to request.
114func NewSinglePager(client *ServiceClient, onlyURL string) Pager {
115 consumed := false
116 single := func(_ string) (Page, error) {
117 if !consumed {
118 consumed = true
119 resp, err := request(client, onlyURL)
120 if err != nil {
121 return SinglePage{}, err
122 }
123
124 cp, err := RememberHTTPResponse(resp)
125 if err != nil {
126 return SinglePage{}, err
127 }
128 return SinglePage(cp), nil
129 }
130 return SinglePage{}, ErrPageNotAvailable
131 }
132
133 return Pager{
134 initialURL: "",
135 fetchNextPage: single,
136 }
137}
138
139// NewLinkedPager creates a Pager that uses a "links" element in the JSON response to locate the next page.
140func NewLinkedPager(client *ServiceClient, initialURL string) Pager {
141 fetchNextPage := func(url string) (Page, error) {
142 resp, err := request(client, url)
143 if err != nil {
144 return nil, err
145 }
146
147 cp, err := RememberHTTPResponse(resp)
148 if err != nil {
149 return nil, err
150 }
151
152 return LinkedPage(cp), nil
153 }
154
155 return Pager{
156 initialURL: initialURL,
157 fetchNextPage: fetchNextPage,
158 }
159}
160
Ash Wilsone30b76b2014-09-12 08:36:17 -0400161// EachPage iterates over each page returned by a Pager, yielding one at a time to a handler function.
162// Return "false" from the handler to prematurely stop iterating.
Ash Wilson6b35e502014-09-12 15:15:23 -0400163func (p Pager) EachPage(handler func(Page) (bool, error)) error {
Ash Wilsone30b76b2014-09-12 08:36:17 -0400164 currentURL := p.initialURL
Ash Wilson64d67b22014-09-05 13:04:12 -0400165 for {
Ash Wilson3658d382014-09-15 08:12:33 -0400166 currentPage, err := p.fetchNextPage(currentURL)
Ash Wilson64d67b22014-09-05 13:04:12 -0400167 if err != nil {
168 return err
169 }
Ash Wilsone30b76b2014-09-12 08:36:17 -0400170
Ash Wilson6b35e502014-09-12 15:15:23 -0400171 ok, err := handler(currentPage)
172 if err != nil {
173 return err
174 }
175 if !ok {
Ash Wilsone30b76b2014-09-12 08:36:17 -0400176 return nil
177 }
178
Ash Wilson976d2e62014-09-12 13:29:29 -0400179 currentURL, err = currentPage.NextPageURL()
180 if err != nil {
181 return err
182 }
Ash Wilsone30b76b2014-09-12 08:36:17 -0400183 if currentURL == "" {
184 return nil
185 }
Ash Wilson64d67b22014-09-05 13:04:12 -0400186 }
187}