blob: 1383f2453ebc258f43bcc39020c1e20cad4c145f [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.
23 NextPageURL() string
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 Wilson5bf6f662014-09-12 12:31:17 -040044func (p Pager) EachPage(handler func(Page) bool) 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
52 if !handler(currentPage) {
53 return nil
54 }
55
56 currentURL = currentPage.NextPageURL()
57 if currentURL == "" {
58 return nil
59 }
Ash Wilson64d67b22014-09-05 13:04:12 -040060 }
61}
62
Ash Wilson5bf6f662014-09-12 12:31:17 -040063// AllPages accumulates every page reachable from a Pager into a single Page, for convenience.
64func (p Pager) AllPages() (Page, error) {
65 return nil, errors.New("Wat")
Ash Wilson64d67b22014-09-05 13:04:12 -040066}
67
Ash Wilson5bf6f662014-09-12 12:31:17 -040068// ConcretePage stores generic information derived from an HTTP response.
69type ConcretePage struct {
70 http.Header
71 Body map[string]interface{}
72}
Ash Wilson64d67b22014-09-05 13:04:12 -040073
Ash Wilson5bf6f662014-09-12 12:31:17 -040074// NewConcretePage parses an HTTP response as JSON and returns a ConcretePage containing the results.
75func NewConcretePage(resp http.Response) (ConcretePage, error) {
76 var parsedBody map[string]interface{}
Ash Wilson64d67b22014-09-05 13:04:12 -040077
Ash Wilson5bf6f662014-09-12 12:31:17 -040078 defer resp.Body.Close()
79 rawBody, err := ioutil.ReadAll(resp.Body)
80 if err != nil {
81 return ConcretePage{}, err
82 }
83 err = json.Unmarshal(rawBody, &parsedBody)
84 if err != nil {
85 return ConcretePage{}, err
86 }
87
88 return ConcretePage{Header: resp.Header, Body: parsedBody}, err
89}
90
91// SinglePage is a page that contains all of the results from an operation.
92type SinglePage ConcretePage
93
94// NextPageURL always returns "" to indicate that there are no more pages to return.
95func (current SinglePage) NextPageURL() string {
96 return ""
97}
98
99// NewSinglePager constructs a Pager that "iterates" over a single Page.
100// Supply a function that returns the only page.
101func NewSinglePager(only func() (http.Response, error)) Pager {
102 consumed := false
103 single := func(_ string) (Page, error) {
104 if !consumed {
105 consumed = true
106 resp, err := only()
107 if err != nil {
108 return SinglePage{}, err
109 }
110
111 cp, err := NewConcretePage(resp)
112 if err != nil {
113 return SinglePage{}, err
114 }
115 return SinglePage(cp), nil
116 }
117 return SinglePage{}, ErrPageNotAvailable
118 }
119
120 return Pager{
121 initialURL: "",
122 advance: single,
123 }
124}
125
126// PaginatedLinksPage is a page in a collection that provides navigational "Next" and "Previous" links within its result.
127type PaginatedLinksPage ConcretePage
128
129// NextPageURL extracts the pagination structure from a JSON response and returns the "next" link, if one is present.
130func (current PaginatedLinksPage) NextPageURL() string {
131 type response struct {
132 Links struct {
133 Next *string `mapstructure:"next,omitempty"`
134 } `mapstructure:"links"`
135 }
136
137 var r response
138 err := mapstructure.Decode(current.Body, &r)
139 if err != nil {
140 // FIXME NextPageURL should be able to fail
141 panic(err)
142 }
143
144 if r.Links.Next == nil {
145 return ""
146 }
147
148 return *r.Links.Next
149}
150
151// NewLinkedPager creates a Pager that uses a "links" element in the JSON response to locate the next page.
152func NewLinkedPager(initialURL string, request func(string) (http.Response, error)) Pager {
153 advance := func(url string) (Page, error) {
154 resp, err := request(url)
155 if err != nil {
156 return nil, err
157 }
158
159 cp, err := NewConcretePage(resp)
160 if err != nil {
161 return nil, err
162 }
163
164 return PaginatedLinksPage(cp), nil
165 }
166
167 return Pager{
168 initialURL: initialURL,
169 advance: advance,
170 }
Ash Wilson64d67b22014-09-05 13:04:12 -0400171}