blob: d6fa5c492f8f50f9bbe86f7dfb16faedac067867 [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 Wilson5bf6f662014-09-12 12:31:17 -040018// Page must be satisfied by the result type of any resource collection.
Ash Wilsone30b76b2014-09-12 08:36:17 -040019// 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 -040020type Page interface {
Ash Wilson64d67b22014-09-05 13:04:12 -040021
Ash Wilsone30b76b2014-09-12 08:36:17 -040022 // NextPageURL generates the URL for the page of data that follows this collection.
23 // Return "" if no such page exists.
Ash Wilson976d2e62014-09-12 13:29:29 -040024 NextPageURL() (string, error)
Ash Wilson64d67b22014-09-05 13:04:12 -040025}
26
Ash Wilsone30b76b2014-09-12 08:36:17 -040027// Pager knows how to advance through a specific resource collection, one page at a time.
28type Pager struct {
29 initialURL string
Ash Wilson64d67b22014-09-05 13:04:12 -040030
Ash Wilson3658d382014-09-15 08:12:33 -040031 fetchNextPage func(string) (Page, error)
Ash Wilsone30b76b2014-09-12 08:36:17 -040032}
33
34// NewPager constructs a manually-configured pager.
35// 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 -040036func NewPager(initialURL string, fetchNextPage func(string) (Page, error)) Pager {
Ash Wilsone30b76b2014-09-12 08:36:17 -040037 return Pager{
Ash Wilson3658d382014-09-15 08:12:33 -040038 initialURL: initialURL,
39 fetchNextPage: fetchNextPage,
Ash Wilsone30b76b2014-09-12 08:36:17 -040040 }
41}
42
Ash Wilsone30b76b2014-09-12 08:36:17 -040043// EachPage iterates over each page returned by a Pager, yielding one at a time to a handler function.
44// Return "false" from the handler to prematurely stop iterating.
Ash Wilson6b35e502014-09-12 15:15:23 -040045func (p Pager) EachPage(handler func(Page) (bool, error)) error {
Ash Wilsone30b76b2014-09-12 08:36:17 -040046 currentURL := p.initialURL
Ash Wilson64d67b22014-09-05 13:04:12 -040047 for {
Ash Wilson3658d382014-09-15 08:12:33 -040048 currentPage, err := p.fetchNextPage(currentURL)
Ash Wilson64d67b22014-09-05 13:04:12 -040049 if err != nil {
50 return err
51 }
Ash Wilsone30b76b2014-09-12 08:36:17 -040052
Ash Wilson6b35e502014-09-12 15:15:23 -040053 ok, err := handler(currentPage)
54 if err != nil {
55 return err
56 }
57 if !ok {
Ash Wilsone30b76b2014-09-12 08:36:17 -040058 return nil
59 }
60
Ash Wilson976d2e62014-09-12 13:29:29 -040061 currentURL, err = currentPage.NextPageURL()
62 if err != nil {
63 return err
64 }
Ash Wilsone30b76b2014-09-12 08:36:17 -040065 if currentURL == "" {
66 return nil
67 }
Ash Wilson64d67b22014-09-05 13:04:12 -040068 }
69}
70
Ash Wilsone0ba5b72014-09-15 08:13:52 -040071// LastHTTPResponse stores generic information derived from an HTTP response.
72type LastHTTPResponse struct {
Ash Wilson5bf6f662014-09-12 12:31:17 -040073 http.Header
74 Body map[string]interface{}
75}
Ash Wilson64d67b22014-09-05 13:04:12 -040076
Ash Wilsone0ba5b72014-09-15 08:13:52 -040077// RememberHTTPResponse parses an HTTP response as JSON and returns a LastHTTPResponse containing the results.
78func RememberHTTPResponse(resp http.Response) (LastHTTPResponse, error) {
Ash Wilson5bf6f662014-09-12 12:31:17 -040079 var parsedBody map[string]interface{}
Ash Wilson64d67b22014-09-05 13:04:12 -040080
Ash Wilson5bf6f662014-09-12 12:31:17 -040081 defer resp.Body.Close()
82 rawBody, err := ioutil.ReadAll(resp.Body)
83 if err != nil {
Ash Wilsone0ba5b72014-09-15 08:13:52 -040084 return LastHTTPResponse{}, err
Ash Wilson5bf6f662014-09-12 12:31:17 -040085 }
86 err = json.Unmarshal(rawBody, &parsedBody)
87 if err != nil {
Ash Wilsone0ba5b72014-09-15 08:13:52 -040088 return LastHTTPResponse{}, err
Ash Wilson5bf6f662014-09-12 12:31:17 -040089 }
90
Ash Wilsone0ba5b72014-09-15 08:13:52 -040091 return LastHTTPResponse{Header: resp.Header, Body: parsedBody}, err
Ash Wilson5bf6f662014-09-12 12:31:17 -040092}
93
Ash Wilson18f32522014-09-15 08:52:12 -040094func request(client *ServiceClient, url string) (http.Response, error) {
95 resp, err := perigee.Request("GET", url, perigee.Options{
96 MoreHeaders: client.Provider.AuthenticatedHeaders(),
97 OkCodes: []int{200},
98 })
99 if err != nil {
100 return http.Response{}, err
101 }
102 return resp.HttpResponse, nil
103}
104
Ash Wilson5bf6f662014-09-12 12:31:17 -0400105// SinglePage is a page that contains all of the results from an operation.
Ash Wilsone0ba5b72014-09-15 08:13:52 -0400106type SinglePage LastHTTPResponse
Ash Wilson5bf6f662014-09-12 12:31:17 -0400107
108// NextPageURL always returns "" to indicate that there are no more pages to return.
Ash Wilson976d2e62014-09-12 13:29:29 -0400109func (current SinglePage) NextPageURL() (string, error) {
110 return "", nil
Ash Wilson5bf6f662014-09-12 12:31:17 -0400111}
112
113// NewSinglePager constructs a Pager that "iterates" over a single Page.
Ash Wilson18f32522014-09-15 08:52:12 -0400114// Supply the URL to request.
115func NewSinglePager(client *ServiceClient, onlyURL string) Pager {
Ash Wilson5bf6f662014-09-12 12:31:17 -0400116 consumed := false
117 single := func(_ string) (Page, error) {
118 if !consumed {
119 consumed = true
Ash Wilson18f32522014-09-15 08:52:12 -0400120 resp, err := request(client, onlyURL)
Ash Wilson5bf6f662014-09-12 12:31:17 -0400121 if err != nil {
122 return SinglePage{}, err
123 }
124
Ash Wilsone0ba5b72014-09-15 08:13:52 -0400125 cp, err := RememberHTTPResponse(resp)
Ash Wilson5bf6f662014-09-12 12:31:17 -0400126 if err != nil {
127 return SinglePage{}, err
128 }
129 return SinglePage(cp), nil
130 }
131 return SinglePage{}, ErrPageNotAvailable
132 }
133
134 return Pager{
Ash Wilson3658d382014-09-15 08:12:33 -0400135 initialURL: "",
136 fetchNextPage: single,
Ash Wilson5bf6f662014-09-12 12:31:17 -0400137 }
138}
139
Ash Wilson583dc732014-09-12 13:30:05 -0400140// LinkedPage is a page in a collection that provides navigational "Next" and "Previous" links within its result.
Ash Wilsone0ba5b72014-09-15 08:13:52 -0400141type LinkedPage LastHTTPResponse
Ash Wilson5bf6f662014-09-12 12:31:17 -0400142
143// NextPageURL extracts the pagination structure from a JSON response and returns the "next" link, if one is present.
Ash Wilson583dc732014-09-12 13:30:05 -0400144func (current LinkedPage) NextPageURL() (string, error) {
Ash Wilson5bf6f662014-09-12 12:31:17 -0400145 type response struct {
146 Links struct {
147 Next *string `mapstructure:"next,omitempty"`
148 } `mapstructure:"links"`
149 }
150
151 var r response
152 err := mapstructure.Decode(current.Body, &r)
153 if err != nil {
Ash Wilson976d2e62014-09-12 13:29:29 -0400154 return "", err
Ash Wilson5bf6f662014-09-12 12:31:17 -0400155 }
156
157 if r.Links.Next == nil {
Ash Wilson976d2e62014-09-12 13:29:29 -0400158 return "", nil
Ash Wilson5bf6f662014-09-12 12:31:17 -0400159 }
160
Ash Wilson976d2e62014-09-12 13:29:29 -0400161 return *r.Links.Next, nil
Ash Wilson5bf6f662014-09-12 12:31:17 -0400162}
163
164// NewLinkedPager creates a Pager that uses a "links" element in the JSON response to locate the next page.
Ash Wilson18f32522014-09-15 08:52:12 -0400165func NewLinkedPager(client *ServiceClient, initialURL string) Pager {
Ash Wilson3658d382014-09-15 08:12:33 -0400166 fetchNextPage := func(url string) (Page, error) {
Ash Wilson18f32522014-09-15 08:52:12 -0400167 resp, err := request(client, url)
Ash Wilson5bf6f662014-09-12 12:31:17 -0400168 if err != nil {
169 return nil, err
170 }
171
Ash Wilsone0ba5b72014-09-15 08:13:52 -0400172 cp, err := RememberHTTPResponse(resp)
Ash Wilson5bf6f662014-09-12 12:31:17 -0400173 if err != nil {
174 return nil, err
175 }
176
Ash Wilson583dc732014-09-12 13:30:05 -0400177 return LinkedPage(cp), nil
Ash Wilson5bf6f662014-09-12 12:31:17 -0400178 }
179
180 return Pager{
Ash Wilson3658d382014-09-15 08:12:33 -0400181 initialURL: initialURL,
182 fetchNextPage: fetchNextPage,
Ash Wilson5bf6f662014-09-12 12:31:17 -0400183 }
Ash Wilson64d67b22014-09-05 13:04:12 -0400184}