blob: ba839656b276514a60e09c69211baceb26cace62 [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 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
Ash Wilson976d2e62014-09-12 13:29:29 -040056 currentURL, err = currentPage.NextPageURL()
57 if err != nil {
58 return err
59 }
Ash Wilsone30b76b2014-09-12 08:36:17 -040060 if currentURL == "" {
61 return nil
62 }
Ash Wilson64d67b22014-09-05 13:04:12 -040063 }
64}
65
Ash Wilson5bf6f662014-09-12 12:31:17 -040066// ConcretePage stores generic information derived from an HTTP response.
67type ConcretePage struct {
68 http.Header
69 Body map[string]interface{}
70}
Ash Wilson64d67b22014-09-05 13:04:12 -040071
Ash Wilson5bf6f662014-09-12 12:31:17 -040072// NewConcretePage parses an HTTP response as JSON and returns a ConcretePage containing the results.
73func NewConcretePage(resp http.Response) (ConcretePage, error) {
74 var parsedBody map[string]interface{}
Ash Wilson64d67b22014-09-05 13:04:12 -040075
Ash Wilson5bf6f662014-09-12 12:31:17 -040076 defer resp.Body.Close()
77 rawBody, err := ioutil.ReadAll(resp.Body)
78 if err != nil {
79 return ConcretePage{}, err
80 }
81 err = json.Unmarshal(rawBody, &parsedBody)
82 if err != nil {
83 return ConcretePage{}, err
84 }
85
86 return ConcretePage{Header: resp.Header, Body: parsedBody}, err
87}
88
89// SinglePage is a page that contains all of the results from an operation.
90type SinglePage ConcretePage
91
92// NextPageURL always returns "" to indicate that there are no more pages to return.
Ash Wilson976d2e62014-09-12 13:29:29 -040093func (current SinglePage) NextPageURL() (string, error) {
94 return "", nil
Ash Wilson5bf6f662014-09-12 12:31:17 -040095}
96
97// NewSinglePager constructs a Pager that "iterates" over a single Page.
98// Supply a function that returns the only page.
99func NewSinglePager(only func() (http.Response, error)) Pager {
100 consumed := false
101 single := func(_ string) (Page, error) {
102 if !consumed {
103 consumed = true
104 resp, err := only()
105 if err != nil {
106 return SinglePage{}, err
107 }
108
109 cp, err := NewConcretePage(resp)
110 if err != nil {
111 return SinglePage{}, err
112 }
113 return SinglePage(cp), nil
114 }
115 return SinglePage{}, ErrPageNotAvailable
116 }
117
118 return Pager{
119 initialURL: "",
120 advance: single,
121 }
122}
123
Ash Wilson583dc732014-09-12 13:30:05 -0400124// LinkedPage is a page in a collection that provides navigational "Next" and "Previous" links within its result.
125type LinkedPage ConcretePage
Ash Wilson5bf6f662014-09-12 12:31:17 -0400126
127// 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 -0400128func (current LinkedPage) NextPageURL() (string, error) {
Ash Wilson5bf6f662014-09-12 12:31:17 -0400129 type response struct {
130 Links struct {
131 Next *string `mapstructure:"next,omitempty"`
132 } `mapstructure:"links"`
133 }
134
135 var r response
136 err := mapstructure.Decode(current.Body, &r)
137 if err != nil {
Ash Wilson976d2e62014-09-12 13:29:29 -0400138 return "", err
Ash Wilson5bf6f662014-09-12 12:31:17 -0400139 }
140
141 if r.Links.Next == nil {
Ash Wilson976d2e62014-09-12 13:29:29 -0400142 return "", nil
Ash Wilson5bf6f662014-09-12 12:31:17 -0400143 }
144
Ash Wilson976d2e62014-09-12 13:29:29 -0400145 return *r.Links.Next, nil
Ash Wilson5bf6f662014-09-12 12:31:17 -0400146}
147
148// NewLinkedPager creates a Pager that uses a "links" element in the JSON response to locate the next page.
149func NewLinkedPager(initialURL string, request func(string) (http.Response, error)) Pager {
150 advance := func(url string) (Page, error) {
151 resp, err := request(url)
152 if err != nil {
153 return nil, err
154 }
155
156 cp, err := NewConcretePage(resp)
157 if err != nil {
158 return nil, err
159 }
160
Ash Wilson583dc732014-09-12 13:30:05 -0400161 return LinkedPage(cp), nil
Ash Wilson5bf6f662014-09-12 12:31:17 -0400162 }
163
164 return Pager{
165 initialURL: initialURL,
166 advance: advance,
167 }
Ash Wilson64d67b22014-09-05 13:04:12 -0400168}