blob: 1830ff0c5cc68171cf8274c5597a069103cf9fb4 [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"
10)
Ash Wilson64d67b22014-09-05 13:04:12 -040011
12var (
13 // ErrPageNotAvailable is returned from a Pager when a next or previous page is requested, but does not exist.
14 ErrPageNotAvailable = errors.New("The requested Collection page does not exist.")
15)
16
Ash Wilson5bf6f662014-09-12 12:31:17 -040017// Page must be satisfied by the result type of any resource collection.
Ash Wilsone30b76b2014-09-12 08:36:17 -040018// 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 -040019type Page interface {
Ash Wilson64d67b22014-09-05 13:04:12 -040020
Ash Wilsone30b76b2014-09-12 08:36:17 -040021 // NextPageURL generates the URL for the page of data that follows this collection.
22 // Return "" if no such page exists.
Ash Wilson976d2e62014-09-12 13:29:29 -040023 NextPageURL() (string, error)
Ash Wilson64d67b22014-09-05 13:04:12 -040024}
25
Ash Wilsone30b76b2014-09-12 08:36:17 -040026// Pager knows how to advance through a specific resource collection, one page at a time.
27type Pager struct {
28 initialURL string
Ash Wilson64d67b22014-09-05 13:04:12 -040029
Ash Wilson5bf6f662014-09-12 12:31:17 -040030 advance func(string) (Page, error)
Ash Wilsone30b76b2014-09-12 08:36:17 -040031}
32
33// NewPager constructs a manually-configured pager.
34// Supply the URL for the first page and a function that requests a specific page given a URL.
Ash Wilson5bf6f662014-09-12 12:31:17 -040035func NewPager(initialURL string, advance func(string) (Page, error)) Pager {
Ash Wilsone30b76b2014-09-12 08:36:17 -040036 return Pager{
37 initialURL: initialURL,
38 advance: advance,
39 }
40}
41
Ash Wilsone30b76b2014-09-12 08:36:17 -040042// EachPage iterates over each page returned by a Pager, yielding one at a time to a handler function.
43// Return "false" from the handler to prematurely stop iterating.
Ash Wilson6b35e502014-09-12 15:15:23 -040044func (p Pager) EachPage(handler func(Page) (bool, error)) error {
Ash Wilsone30b76b2014-09-12 08:36:17 -040045 currentURL := p.initialURL
Ash Wilson64d67b22014-09-05 13:04:12 -040046 for {
Ash Wilsone30b76b2014-09-12 08:36:17 -040047 currentPage, err := p.advance(currentURL)
Ash Wilson64d67b22014-09-05 13:04:12 -040048 if err != nil {
49 return err
50 }
Ash Wilsone30b76b2014-09-12 08:36:17 -040051
Ash Wilson6b35e502014-09-12 15:15:23 -040052 ok, err := handler(currentPage)
53 if err != nil {
54 return err
55 }
56 if !ok {
Ash Wilsone30b76b2014-09-12 08:36:17 -040057 return nil
58 }
59
Ash Wilson976d2e62014-09-12 13:29:29 -040060 currentURL, err = currentPage.NextPageURL()
61 if err != nil {
62 return err
63 }
Ash Wilsone30b76b2014-09-12 08:36:17 -040064 if currentURL == "" {
65 return nil
66 }
Ash Wilson64d67b22014-09-05 13:04:12 -040067 }
68}
69
Ash Wilson5bf6f662014-09-12 12:31:17 -040070// ConcretePage stores generic information derived from an HTTP response.
71type ConcretePage struct {
72 http.Header
73 Body map[string]interface{}
74}
Ash Wilson64d67b22014-09-05 13:04:12 -040075
Ash Wilson5bf6f662014-09-12 12:31:17 -040076// NewConcretePage parses an HTTP response as JSON and returns a ConcretePage containing the results.
77func NewConcretePage(resp http.Response) (ConcretePage, error) {
78 var parsedBody map[string]interface{}
Ash Wilson64d67b22014-09-05 13:04:12 -040079
Ash Wilson5bf6f662014-09-12 12:31:17 -040080 defer resp.Body.Close()
81 rawBody, err := ioutil.ReadAll(resp.Body)
82 if err != nil {
83 return ConcretePage{}, err
84 }
85 err = json.Unmarshal(rawBody, &parsedBody)
86 if err != nil {
87 return ConcretePage{}, err
88 }
89
90 return ConcretePage{Header: resp.Header, Body: parsedBody}, err
91}
92
93// SinglePage is a page that contains all of the results from an operation.
94type SinglePage ConcretePage
95
96// NextPageURL always returns "" to indicate that there are no more pages to return.
Ash Wilson976d2e62014-09-12 13:29:29 -040097func (current SinglePage) NextPageURL() (string, error) {
98 return "", nil
Ash Wilson5bf6f662014-09-12 12:31:17 -040099}
100
101// NewSinglePager constructs a Pager that "iterates" over a single Page.
102// Supply a function that returns the only page.
103func NewSinglePager(only func() (http.Response, error)) Pager {
104 consumed := false
105 single := func(_ string) (Page, error) {
106 if !consumed {
107 consumed = true
108 resp, err := only()
109 if err != nil {
110 return SinglePage{}, err
111 }
112
113 cp, err := NewConcretePage(resp)
114 if err != nil {
115 return SinglePage{}, err
116 }
117 return SinglePage(cp), nil
118 }
119 return SinglePage{}, ErrPageNotAvailable
120 }
121
122 return Pager{
123 initialURL: "",
124 advance: single,
125 }
126}
127
Ash Wilson583dc732014-09-12 13:30:05 -0400128// LinkedPage is a page in a collection that provides navigational "Next" and "Previous" links within its result.
129type LinkedPage ConcretePage
Ash Wilson5bf6f662014-09-12 12:31:17 -0400130
131// 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 -0400132func (current LinkedPage) NextPageURL() (string, error) {
Ash Wilson5bf6f662014-09-12 12:31:17 -0400133 type response struct {
134 Links struct {
135 Next *string `mapstructure:"next,omitempty"`
136 } `mapstructure:"links"`
137 }
138
139 var r response
140 err := mapstructure.Decode(current.Body, &r)
141 if err != nil {
Ash Wilson976d2e62014-09-12 13:29:29 -0400142 return "", err
Ash Wilson5bf6f662014-09-12 12:31:17 -0400143 }
144
145 if r.Links.Next == nil {
Ash Wilson976d2e62014-09-12 13:29:29 -0400146 return "", nil
Ash Wilson5bf6f662014-09-12 12:31:17 -0400147 }
148
Ash Wilson976d2e62014-09-12 13:29:29 -0400149 return *r.Links.Next, nil
Ash Wilson5bf6f662014-09-12 12:31:17 -0400150}
151
152// NewLinkedPager creates a Pager that uses a "links" element in the JSON response to locate the next page.
153func NewLinkedPager(initialURL string, request func(string) (http.Response, error)) Pager {
154 advance := func(url string) (Page, error) {
155 resp, err := request(url)
156 if err != nil {
157 return nil, err
158 }
159
160 cp, err := NewConcretePage(resp)
161 if err != nil {
162 return nil, err
163 }
164
Ash Wilson583dc732014-09-12 13:30:05 -0400165 return LinkedPage(cp), nil
Ash Wilson5bf6f662014-09-12 12:31:17 -0400166 }
167
168 return Pager{
169 initialURL: initialURL,
170 advance: advance,
171 }
Ash Wilson64d67b22014-09-05 13:04:12 -0400172}